史蒂夫·乔布斯(Steve Jobs)曾称计算机为“心灵的自行车”。 然而,当他谈到地球上所有物种的运动效率时,人们对他的隐喻的背景知之甚少。
由 Dall·e 3** 生成,提示“将计算机视为心灵的自行车”。
秃鹫获胜并位居榜首,超过了所有其他物种。 人类是......在大约三分之一的列表中但是一旦人类骑上自行车,它们就可以远远超过秃鹫并登上榜首。 它启发了我,人类是工具制造者,我们可以制造工具,将这些固有的能力放大到惊人的程度。 对我来说,计算机一直是大脑的自行车,它让我们远远超出了我们的固有能力。 我认为我们只是处于这个工具的早期阶段,非常早期的阶段。 我们只走了很短的距离,它仍处于形成阶段,但我们已经看到了巨大的变化。 在我看来,与未来 100 年将发生的事情相比,这算不了什么。
史蒂夫·乔布斯(1990)。
#
谨慎乐观
LLM 在加速软件开发中的作用已被广泛讨论。 有人认为,自动生成的**的质量如此之低,以至于使用这些**会产生负面影响。 另一方面,许多人声称编程时代已经结束。 有许多研究试图客观地评估 LLM 在 **质量基准数据集(如 Humaneval 或 MBPP)上的性能。 这些评估对于该领域的发展至关重要,但它们不是本文的重点。
本文的目的是为开发人员提供实用建议,尤其是那些对利用这些模型持保留态度的开发人员。 需要注意的是,我相信,通过正确使用这项技术,一个人的生产力确实可以实现质的飞跃
需要明确的是,使用 LLM 并不一定意味着您将产生高性能**。 即使是最先进的模型也会犯严重的错误。 但重要的是要记住,最好的人类工程师也会犯错误。 这就是为什么我们不会在没有护栏的情况下发布,例如自动化测试和同行评审。 LLM 不会改变最佳实践的性质,但它们可能会改变它们实施的难易程度。
描述现在交付 ** 的过程(**由 dall·e 3 生成)。
写高性能**速度真的那么重要吗?
如果大多数专业软件工程师把大部分时间都花在键盘上打字,我会感到惊讶。 如果是这样的话,我们可能会更认真地将“行数”作为性能的衡量标准。 我认为工程师实际上花了更多的时间来决定先写哪些。 目前,LLM没有能力进行这种高级抽象思维——OpenAI和许多其他机构正在努力使这些步骤成为可能,但这很可能需要的不仅仅是增加模型的参数或扩展训练数据,而是一种全新的思维方式。 然而,LLM已经能够在很大程度上加快我们的大部分工作。
#
编写高效函数
让我举个例子,说明我们需要采取哪些步骤来优化 LLM,以便在我们生成一个小段后使其有用。 测量地球上的一点与另一组坐标之间的距离是地理空间分析中的常见任务。 想象一下这样的场景。
您看到的第一个输出是这样的:
import mathdef h**ersine(lat1, lon1, lat2, lon2): # radius of the earth in kilometers r = 6371.0 # convert degrees to radians lat1_rad = math.radians(lat1) lon1_rad = math.radians(lon1) lat2_rad = math.radians(lat2) lon2_rad = math.radians(lon2) # differences in coordinates dlat = lat2_rad - lat1_rad dlon = lon2_rad - lon1_rad # h**ersine formula a = math.sin(dlat / 2)**2 + math.cos(lat1_rad) *math.cos(lat2_rad) *math.sin(dlon / 2)**2 c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) # distance in kilometers distance = r * c return distancedef find_closest_oil_platform(vessel_lat, vessel_lon, platforms): closest_platform = none min_distance = float('inf') for platform in platforms: distance = h**ersine(vessel_lat, vessel_lon, platform['latitude'], platform['longitude']) if distance < min_distance: min_distance = distance closest_platform = platform return closest_platform, min_distance
那么,这个**的质量呢?
这一段的质量并不理想。 它有很多问题,最明显的是它难以阅读,因为它使用了难以理解和不一致的变量命名,包括大写和小写的单字符命名。 此外,它缺乏文档和类型提示,使用不必要且低效的 for 循环,并且不处理等距离等特殊情况。
但是修改后:
from typing import tuple, listimport numpy as npdef h**ersine_vectorized(vessel_lat: float, vessel_lon: float, platform_lats: np.ndarray, platform_lons: np.ndarray) -np.ndarray: """ calculate the h**ersine distance between a vessel and multiple platforms in a vectorized manner. parameters: -vessel_lat: latitude of the vessel in decimal degrees. -vessel_lon: longitude of the vessel in decimal degrees. -platform_lats: numpy array of latitudes of oil platforms in decimal degrees. -platform_lons: numpy array of longitudes of oil platforms in decimal degrees. returns: -distances: numpy array of distances from the vessel to each platform in kilometers. """ # convert decimal degrees to radians lat1, lon1, lat2, lon2 = map(np.radians, [vessel_lat, vessel_lon, platform_lats, platform_lons]) # h**ersine formula dlat = lat2 - lat1 dlon = lon2 - lon1 a = np.sin(dlat/2)**2 + np.cos(lat1) *np.cos(lat2) *np.sin(dlon/2)**2 c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1-a)) r = 6371 # radius of earth in kilometers return c * rdef find_closest_platform(vessel_lat: float, vessel_lon: float, platform_lats: np.ndarray, platform_lons: np.ndarray) -tuple[list[int], float]: """ finds the closest oil platform(s) to a vessel given arrays of platform latitudes and longitudes, handling equidistant platforms. parameters: -vessel_lat: latitude of the vessel in decimal degrees. -vessel_lon: longitude of the vessel in decimal degrees. -platform_lats: numpy array of latitudes for oil platforms. -platform_lons: numpy array of longitudes for oil platforms. returns: -a tuple containing a list of indices of the closest platforms and the distance to them in kilometers. """ # calculate distances to all platforms distances = h**ersine_vectorized(vessel_lat, vessel_lon, platform_lats, platform_lons) # find the minimum distance min_distance = np.min(distances) # find all indices with the minimum distance closest_indices = np.where(distances == min_distance)[0].tolist() # return the indices of all closest platforms and the minimum distance return closest_indices, min_distance
改进后的**得到了显着改进。 它更易于阅读,添加了文档和类型提示,并用更有效的计算向量的方式替换了 for 循环。
然而,“好”,更重要的是,它是否符合要求取决于它运行的具体环境。 你知道,你不能只用几行字有效地评估它的质量,对人类和LLM来说都是如此。
例如,这一段的准确性是否符合用户的期望? 它会经常运行吗? 是一年一次,还是每微秒一次? 使用的硬件条件是什么? 预期的使用量和规模是否值得进行小规模的优化? 考虑到你的薪水,这样做是否划算?
让我们根据上述因素来评估这一段。
在准确性方面,虽然 h**ersine 公式表现良好,但它并不是最佳选择,因为它将地球视为一个完美的球体,而实际上地球更接近一个扁球体。 当需要在很远的距离上进行毫米级精度的测量时,这种差异就变得很重要。 如果确实需要这种精确度,可以使用更精确的公式,例如文森特公式,但这需要权衡性能。 由于本部分的用户不需要毫米级精度(实际上,由于从卫星图像得出的船舶坐标误差,这种精度无关紧要),因此就精度而言,半正弦函数是合理的选择。
* 它运行得够快吗? 考虑到只需要计算几千个海上石油平台,特别是通过矢量计算方法,这种计算非常有效。 但是,如果应用程序变成计算到海岸上任何一点的距离(海岸线上有数亿个点),那么“分而治之”的策略可能更合适。 在实践中,考虑到需要节省计算成本,此函数设计为每天在成本最高的低配置虚拟机上运行约 1 亿次。
基于这些详细的背景信息,我们可以认为上面的**实现是合理的。 这也意味着在最终合并之前,应该对其进行测试(我通常不建议仅依赖 LLM)和人类同行评审。
#
加速前进
它不仅通过使用 LLM 像以前一样自动生成有用的函数来节省您的时间,而且当您开始使用它们来生成整个库、处理模块之间的依赖关系、编写文档、可视化(通过多模态功能)、编写自述文件、开发命令行界面等时,它们带来的价值会呈指数级增长。
让我们尝试在 LLM 的帮助下从头开始创建、训练、评估和推断新的计算机视觉模型。 以最近发表的一篇文章为例,“通过深度学习识别 Sentinel-2 图像中船舶尾流组件的临界点方法”(Del Prete 等人,IEEE GRSL,2023 年),这是推动和激励我们前进的动力。
*链接:
Sentinel-2 卫星图像中显示的船只及其尾流。
为什么我们需要关心卫星图像中船的方向,这项任务的难点是什么?
对于需要监测水域中人类活动的组织来说,通过静态图像识别船只的方向是非常有价值的信息。 例如,如果一艘船驶向海洋保护区,这可能意味着需要采取警惕或拦截措施。 通常,全球发布的卫星图像的分辨率不足以准确确定船舶的方向,尤其是那些只在图像上占据几个像素的小型船只(例如,Sentinel-2的图像分辨率为10米像素)。 然而,即使是小船也会在水中留下相当明显的涟漪,即使无法直接识别船尾,也能为我们提供船只前进方向的线索。
这项研究之所以引人注目,是因为它使用了基于EfficientNetB0的模型,该模型足够小,可以在不花费太多计算资源的情况下大规模应用。 虽然我没有找到具体的实现,但作者公开了包括标记在内的数据集,这是一个可观的步骤。
让我们开始探索吧!
与任何新的机器学习项目一样,首先可视化数据是一个具有启发性的步骤。
import osimport jsonfrom pil import image, imagedrawimport matplotlib.pyplot as pltimport seaborn as sns# define the path to your data directorydata_dir = "/path/to/your/data" # adjust this to the path of your data directoryannotations_dir = os.path.join(data_dir, "annotations")images_dir = os.path.join(data_dir, "imgs")# initialize seaborn for better visual aestheticssns.set(style="whitegrid", palette="muted")# create a list to hold file paths for images and their corresponding annotationsimage_files = annotation_files = # loop through the annotations directory to get the list of annotation filesfor annotation_file in os.listdir(annotations_dir): if annotation_file.endswith(".json"): annotation_path = os.path.join(annotations_dir, annotation_file) image_file = annotation_file.replace(".json", ".png") # assuming image file names match annotation file names image_path = os.path.join(images_dir, image_file) # check if the corresponding image file exists if os.path.exists(image_path): annotation_files.append(annotation_path) image_files.append(image_path)# plottingnum_examples = min(len(image_files), 10) # limiting to 10 examples for visualizationfig, axes = plt.subplots(2, 5, figsize=(20, 8))for idx, (image_path, annotation_path) in enumerate(zip(image_files[:num_examples], annotation_files[:num_examples]))# load the image img = image.open(image_path).convert("rgb") # ensure the image is treated as rgb draw = imagedraw.draw(img) # load the corresponding annotations and draw keypoints with open(annotation_path, 'r') as f: annotations = json.load(f) for point in annotations["tooltips"]: x, y = point["x"], point["y"] # draw keypoints in cyan for visibility draw.ellipse([(x-10, y-10), x+10, y+10)],fill='cyan', outline='black') # plot the image with keypoints ax = axes[idx // 5, idx % 5] ax.imshow(img) ax.axis('off') ax.set_title(f"image ")plt.tight_layout()plt.show()
结果如下:
Sentinel-2 卫星图像中的船只和尾流关键点注释(青色)样本。
结果显示,不同**中标记的关键点(青点)数量不同; 其次,图像都是灰度的,形状各异; 第三,虽然样本量很小,但其内部差异相当大。 考虑到这些基础,我们将编写加载数据集、定义神经网络、训练神经网络并验证它的基础。 在请求之前,我们将向 LLM 提供一些背景信息。
这将导致以下输出:
import osimport jsonimport torchfrom torch.utils.data import dataset, dataloaderfrom torchvision import transformsfrom torchvision.io import read_imagefrom typing import tuple, listclass wakedataset(dataset): """ custom dataset for loading wake detection imagery and annotations, with preprocessing for efficientnet. images are grayscale and will be converted to three-channel images for model compatibility. """ def __init__(self, annotations_dir: str, images_dir: str, transform=none): """ initializes the dataset with paths to annotations and images directories, and a transform. parameters: -annotations_dir: path to the directory containing annotation json files. -images_dir: path to the directory containing grayscale image files. -transform: a torchvision.transforms transform to be applied on a sample. """ self.annotations_dir = annotations_dir self.images_dir = images_dir self.transform = transform if transform else self.default_transform() self.annotations = self._load_annotations() def default_transform(self): """ defines the default transform to resize images, convert them to three-channel format, and to tensors. returns: -a torchvision.transforms composition for image preprocessing. """ return transforms.compose([ transforms.resize((224, 224)),# resize to the input size for efficientnet transforms.grayscale(num_output_channels=3), # convert grayscale to three-channel transforms.convertimagedtype(torch.float), # ensures image is a float tensor ])def _load_annotations(self) -list[tuple[str, list[int]]]""" loads and filters annotations with at least 2 keypoints. returns: -a list of tuples, where each tuple contains the image filename and its keypoints. """ annotations = for filename in os.listdir(self.annotations_dir): if not filename.endswith('.json'): continue filepath = os.path.join(self.annotations_dir, filename) with open(filepath, 'r') as f: data = json.load(f) if len(data['tooltips']) = 2: # filter annotations with at least 2 keypoints keypoints = [item for sublist in [[kp['x'], kp['y']] for kp in data['tooltips']] for item in sublist] annotations.append((filename.replace('.json', '.png'), keypoints)) return annotations def __len__(self) -int: """ returns the number of items in the dataset. """ return len(self.annotations) def __getitem__(self, idx) -tuple[torch.tensor, torch.tensor]: """ retrieves an item from the dataset at the specified index, with preprocessing for efficientnet. parameters: -idx: index of the item to retrieve. returns: -a tuple containing the preprocessed image tensor and its corresponding keypoints tensor. """ img_name, keypoints = self.annotations[idx] img_path = os.path.join(self.images_dir, img_name) image = read_image(img_path) # load image keypoints = torch.tensor(keypoints + 0] *6 - len(keypoints)),dtype=torch.float) # pad keypoints if self.transform: image = self.transform(image) return image, keypoints
结果 ** 看起来相当合理,并且似乎注意到了问题的一些细节。 虽然并不完美(例如,选择在默认变换中将图像从灰度转换为 RGB 颜色模式是一个奇怪的决定,但我个人更喜欢使用 Pathlib 而不是 OS。 listdir),但我们可以暂时把这些细节放在一边。
import torchimport torch.nn as nnfrom torchvision import modelsfrom typing import tupleclass efficientnetb0keypointdetector(nn.module): """ a custom efficientnet-b0 model for keypoint detection in grayscale images. the network modifies the input layer to accept single-channel images and adds a custom output layer for predicting a fixed number of keypoints. """ def __init__(self, num_keypoints: int = 3): """ initializes the model. parameters: -num_keypoints: the number of keypoints to predict. default is 3, corresponding to the maximum number of keypoints in the dataset. """ super(efficientnetb0keypointdetector, self)._init__(self.num_keypoints = num_keypoints # load a pre-trained efficientnet-b0 self.efficientnet_b0 = models.efficientnet_b0(pretrained=true) # modify the first convolutional layer to accept single-channel (grayscale) images original_first_conv = self.efficientnet_b0.features[0][0] self.efficientnet_b0.features[0][0] = nn.conv2d( in_channels=1, out_channels=original_first_conv.out_channels, kernel_size=original_first_conv.kernel_size, stride=original_first_conv.stride, padding=original_first_conv.padding, bias=false) # modify the classifier to predict keypoints (num_keypoints * 2 because x, y for each keypoint) self.efficientnet_b0.classifier = nn.linear( in_features=self.efficientnet_b0.classifier[1].in_features, out_features=num_keypoints * 2) def forward(self, x: torch.tensor) -torch.tensor: """ forward pass of the network. parameters: -x: input tensor of shape (batch_size, 1, 224, 224) returns: -a tensor of shape (batch_size, num_keypoints * 2) representing the predicted keypoints. """ return self.efficientnet_b0(x)# example usageif __name__ == "__main__": model = efficientnetb0keypointdetector() print(model) # example input tensor (batch_size, channels, height, width) example_input = torch.randn(1, 1, 224, 224) output = model(example_input) print(output.shape) # expected shape: (1, 6) for 3 keypoints (each with x, y coordinates)
import osimport torchimport torch.nn as nnimport torch.optim as optimfrom torch.utils.data import dataloaderfrom dataset import wakedataset, get_dataloader # assuming dataset.py is in the same directoryfrom net import efficientnetb0keypointdetectorfrom torchvision import transformsimport wandb# initialize weights & biaseswandb.init(project="wake_model_llm_assist", entity="your_wandb_entity", mode="offline")def custom_collate_fn(batch): """ custom collate function to pad keypoints to a fixed size. parameters: -batch: a list of tuples (image, keypoints). returns: -batched images and keypoints with padding as necessary. """ images, keypoints = zip(*batch) images = torch.stack(images, 0) max_keypoints = 3 * 2 # maximum number of keypoints (3) times 2 for (x, y) padded_keypoints = [torch.cat((kp, torch.zeros(max_keypoints - len(kp)))for kp in keypoints] keypoints = torch.stack(padded_keypoints, 0) return images, keypointsdef train_one_epoch(model: nn.module, dataloader: dataloader, optimizer: optim.optimizer, device: torch.device): """ trains the model for one epoch. parameters: -model: the neural network model. -dataloader: dataloader providing the training data. -optimizer: optimizer used for model training. -device: the device to train on. """ model.train() total_loss = 0.0 for images, keypoints in dataloader: images, keypoints = images.to(device), keypoints.to(device) optimizer.zero_grad() outputs = model(images) loss = nn.mseloss()(outputs, keypoints) loss.backward() optimizer.step() total_loss += loss.item() erage_loss = total_loss / len(dataloader) wandb.log()def evaluate(model: nn.module, dataloader: dataloader, device: torch.device): """ evaluates the model on the validation set. parameters: -model: the neural network model. -dataloader: dataloader providing the validation data. -device: the device to evaluate on. """ model.eval() total_loss = 0.0 with torch.no_grad():for images, keypoints in dataloader: images, keypoints = images.to(device), keypoints.to(device) outputs = model(images) loss = nn.mseloss()(outputs, keypoints) total_loss += loss.item() erage_loss = total_loss / len(dataloader) wandb.log()def main():# setup device = torch.device("cuda" if torch.cuda.is_**ailable() else "cpu") model = efficientnetb0keypointdetector().to(device) optimizer = optim.adam(model.parameters(),lr=1e-3) # dataloaders train_dataloader = get_dataloader('path/to/train/annotations', 'path/to/train/images', batch_size=4, transform=transforms.totensor(),collate_fn=custom_collate_fn) val_dataloader = get_dataloader('path/to/val/annotations', 'path/to/val/images', batch_size=4, transform=transforms.totensor(),collate_fn=custom_collate_fn) # training loop for epoch in range(num_epochs): train_one_epoch(model, train_dataloader, optimizer, device) evaluate(model, val_dataloader, device) # log model checkpoints, images, or other metrics as needed wandb.log({}if __name__ == "__main__": main()
当我开始尝试运行时,我遇到了一堆与张量形状不匹配相关的问题(记住之前的 rgb 与灰度图像以及自定义整理函数的问题)。 我尝试了几分钟的调试,但最终选择将所有模块复制到工具提示中,让它帮助我找出问题所在。
这样做有效地解决了任何悬而未决的问题,我开始了培训......的模型但是训练速度却出奇地慢。 就在那时,我意识到我没有利用 Apple 芯片上的金属性能着色器 (MPS) 后端。 虽然我通常不会在 PC 上训练模型,而且 MPS 对我来说还比较新,但我决定添加一个条件判断来使用它。
考虑到适度的训练数据(总共 581 张照片**)以及 EfficientNet 已经在 ImageNet 上进行了预训练,我决定进行 1000 个周期的训练。
经过 500 个周期后,列车损耗仍在下降,但验证损耗似乎已经收敛(至少足以进行快速评估)。 图像是根据权重和偏差复制的。
值得注意的是,虽然有一些基础模型已经在卫星图像上进行了预训练(相对于ImageNet),这些模型可能对这项任务更有效,但这些网络比EfficientNet大得多,因此训练速度较慢(而且太新,无法包含在LLM训练数据集中)。
现在,让我们编写一个推理脚本和一个命令行界面 (CLI) 来测试我们刚刚训练的模型。
import torchfrom torchvision import transformsfrom pil import imageimport matplotlib.pyplot as pltimport click# assuming net.py contains the efficientnetb0keypointdetector class# and dataset.py contains the transforms used during trainingfrom net import efficientnetb0keypointdetectorfrom dataset import wakedataset # modify as necessary to import transformsdef load_model(model_path): """load the trained model from a file.""" model = efficientnetb0keypointdetector() model.load_state_dict(torch.load(model_path)) model.eval() # set the model to inference mode return modeldef preprocess_image(image_path, transform): """load and preprocess an image.""" image = image.open(image_path).convert("l") # assuming grayscale conversion as in your dataset image = transform(image) # add batch dimension (bxcxhxw) image = image.unsqueeze(0) return imagedef plot_keypoints(image, keypoints): """plot keypoints on the image.""" plt.imshow(image.squeeze(),cmap='gray') # remove batch dimension and show image plt.scatter(keypoints[:,0], keypoints[:,1], s=50, marker='.', c='red') plt.show()@click.command()@click.argument('model_path', type=click.path(exists=true))@click.argument('image_path', type=click.path(exists=true))def run_inference(model_path, image_path): """run inference on an image using a trained model.""" # use the same transforms as during training transform = transforms.compose([ transforms.resize((224, 224)),transforms.totensor(),transforms.grayscale(num_output_channels=3), model = load_model(model_path) image = preprocess_image(image_path, transform) # perform inference with torch.no_grad():keypoints = model(image) keypoints = keypoints.view(-1, 2).cpu().numpy() # reshape and convert to numpy for plotting # load original image for plotting original_image = image.open(image_path).convert("l") plot_keypoints(original_image, keypoints)if __name__ == '__main__': run_inference()
让我们开始吧!
它并不完美,但对于第一次通过来说是合理的。
您可以在 GitHub 上找到完整的自述文件,其中包含所有模块、模型和权重(用于周期 500)和自述文件。 我花了不到一个小时的时间就生成了整个库,这个过程比写这篇文章花费的时间要少得多。 所有这些工作都是在我的个人开发环境中完成的:MacBook Air M2 + VS Code + Copilot + 保存时自动格式化(使用 Black、Isort 等)+ 一个 Python 39.6 的虚拟环境 (..)venv)。
github:
经验 教训
为模型提供尽可能多的相关上下文,以帮助其解决任务。 请记住,该模型缺少许多您可能认为理所当然的假设。 生成的 LLM 通常远非完美,而且失败的方式具有挑战性。 因此,在 IDE 中有一个辅助工具(例如 Copilot)是非常有帮助的。 当你严重依赖 LLM 时,请记住,你的写作速度通常是限制因素。 避免不需要任何更改的重复请求,这不仅会浪费精力,还会减慢您的进度。 LLM 很难“记住”它们输出的每一行,并且经常需要提醒它们当前状态(尤其是当存在跨越多个模块的依赖项时)。 对 LLM 生成的 ** 持怀疑态度。 使用测试、可视化等方式尽可能多地进行验证。 并将时间投入到重要的事情上。 我花在h**ersine函数上的时间比花在神经部分的时间要多(因为预期的规模需要更多的性能),而对于神经网络,我更关心的是快速发现故障。 #
法学硕士和工程学的未来
只有变化是永恒的。
赫拉克利特。
在LLM引发的繁荣和巨额现金流的背景下,人们很容易首先期待完美。 然而,有效利用这些工具需要我们进行实验、学习和适应。
LLM会改变软件工程团队的基本结构吗? 也许,我们现在只是新世界前的一条小径。 但法学硕士已经使访问民主化。 即使是没有编程经验的人也可以快速轻松地构建功能原型。 如果你有严格的要求,在你已经熟悉的领域应用LLM可能更明智。 根据我的个人经验,LLM 可以将高效写作所需的时间**减少约 90%。 如果你发现他们总是输出低质量**,那么也许是时候重新审视你的输入了。
原文链接: