在競爭日益激烈的制造業(yè)與電商領(lǐng)域,每一分成本都至關(guān)重要。您是否還在為產(chǎn)品計數(shù)環(huán)節(jié)而困擾?
-
高價值小零件(如螺絲、珠寶、電子元件)的人工計數(shù),效率低下且易出錯?
-
藥品、保健品瓶裝前的計數(shù),對精度有嚴苛要求,容不得半點馬虎?
-
海量零散物品的分裝與包裝,人工成本高昂,管理困難?
人工計數(shù)的時代,該落幕了。?是時候讓更智能、更可靠的伙伴——視覺計數(shù)包裝機,來接管這項繁瑣而關(guān)鍵的任務(wù)。
核心技術(shù)揭秘:AI的“火眼金睛”是怎樣煉成的?
許多人好奇,這臺機器是如何像人眼一樣,甚至比人眼更精準地識別并數(shù)出成千上萬的物體的?其核心,在于融合了尖端計算機視覺與深度學(xué)習(xí)AI的智能系統(tǒng)。整個過程,可以概括為以下四個精密的步驟:
第一步:高清捕捉,“明察秋毫”
系統(tǒng)首先通過工業(yè)級高分辨率攝像頭,在均勻穩(wěn)定的光源環(huán)境下,對傳送帶或振動盤上的待計數(shù)產(chǎn)品進行快速連續(xù)拍照。這確保了獲取的圖片清晰、無陰影、無畸變,為AI的精準分析打下堅實基礎(chǔ)。
第二步:智能識別,“去偽存真”
這是AI大顯身手的環(huán)節(jié)。經(jīng)過海量數(shù)據(jù)訓(xùn)練的深度學(xué)習(xí)模型,會對圖片進行如下分析:
-
特征提取:?AI模型能夠自動學(xué)習(xí)并識別目標物體的獨特特征,如形狀、大小、顏色、紋理、邊緣輪廓等。無論是圓形的藥片、方形的芯片還是異形的螺絲,它都能精準捕捉其本質(zhì)特征。
-
目標檢測與分割:?AI會像一位經(jīng)驗豐富的老師傅,迅速在圖片中“圈出”每一個獨立的物體,哪怕它們有部分重疊或堆積。先進的算法能夠智能地將粘連的物體區(qū)分開來,極大降低了誤判率。
-
分類過濾:?系統(tǒng)可以設(shè)定規(guī)則,自動忽略背景干擾、灰塵或與目標物形態(tài)迥異的雜質(zhì),確保只計數(shù)正確的產(chǎn)品,實現(xiàn)“去偽存真”。
第三步:精準計數(shù),“分毫不錯”
在成功識別出每一個物體后,AI會對其進行實時標記。系統(tǒng)會以驚人的速度對標記框進行統(tǒng)計,無論是成千上萬的零部件,還是細如發(fā)絲的元器件,都能在瞬間完成計數(shù),速度遠超人工,且精度高達99.9%以上,徹底告別人工計數(shù)的誤差與爭議。

從像素到數(shù)據(jù):圖像識別計數(shù)AI的底層邏輯與算法革新:
/// <summary>
/// 暗區(qū)域檢測數(shù)據(jù)集 - 自動加載圖像和標注文件進行訓(xùn)練
/// 支持多種標注格式并包含針對暗區(qū)域的專用數(shù)據(jù)增強
/// </summary>
public class DarkRegionDataset : IEnumerable<Dictionary<string, Tensor>>, IDisposable
{
private readonly string[] imageFilePaths; // 圖像文件路徑數(shù)組
private readonly string[] annotationFilePaths; // 標注文件路徑數(shù)組
private readonly DarkRegionDetectorConfig config; // 訓(xùn)練配置參數(shù)
private readonly Random randomGenerator; // 隨機數(shù)生成器,用于數(shù)據(jù)增強
private readonly int inputImageSize; // 輸入圖像尺寸
private bool isDisposed = false; // 資源釋放標志
/// <summary>
/// 構(gòu)造函數(shù) - 初始化數(shù)據(jù)集并驗證數(shù)據(jù)完整性
/// </summary>
public DarkRegionDataset(string imagesDirectory, string annotationsDirectory, DarkRegionDetectorConfig config)
{
this.config = config; // 保存配置參數(shù)
this.randomGenerator = new Random(DateTime.Now.Millisecond); // 初始化隨機數(shù)生成器
this.inputImageSize = 640; // 設(shè)置輸入圖像尺寸為640x640
// 加載圖像文件路徑
this.imageFilePaths = Directory.GetFiles(imagesDirectory, "*.jpg") // 獲取所有jpg文件
.Concat(Directory.GetFiles(imagesDirectory, "*.png")) // 獲取所有png文件
.Concat(Directory.GetFiles(imagesDirectory, "*.bmp")) // 獲取所有bmp文件
.OrderBy(path => path) // 按路徑排序確保一致性
.ToArray(); // 轉(zhuǎn)換為數(shù)組
// 加載標注文件路徑
this.annotationFilePaths = Directory.GetFiles(annotationsDirectory, "*.txt") // 獲取所有txt標注文件
.OrderBy(path => path) // 按路徑排序
.ToArray(); // 轉(zhuǎn)換為數(shù)組
// 驗證數(shù)據(jù)完整性
ValidateDatasetIntegrity(); // 檢查圖像和標注文件是否匹配
Console.WriteLine($"數(shù)據(jù)集加載完成: {imageFilePaths.Length} 張圖像, {annotationFilePaths.Length} 個標注文件"); // 輸出加載信息
}
/// <summary>
/// 驗證數(shù)據(jù)集完整性 - 檢查圖像和標注文件是否匹配
/// </summary>
private void ValidateDatasetIntegrity()
{
if (imageFilePaths.Length != annotationFilePaths.Length) // 檢查數(shù)量是否一致
{
throw new InvalidDataException($"圖像文件數(shù)量({imageFilePaths.Length})與標注文件數(shù)量({annotationFilePaths.Length})不匹配"); // 拋出異常
}
// 檢查文件名是否對應(yīng)
for (int i = 0; i < imageFilePaths.Length; i++) // 遍歷所有文件
{
string imageName = Path.GetFileNameWithoutExtension(imageFilePaths[i]); // 獲取圖像文件名(不含擴展名)
string annotationName = Path.GetFileNameWithoutExtension(annotationFilePaths[i]); // 獲取標注文件名(不含擴展名)
if (imageName != annotationName) // 檢查文件名是否一致
{
throw new InvalidDataException($"文件不匹配: {imageName} 與 {annotationName}"); // 拋出異常
}
}
}
/// <summary>
/// 獲取數(shù)據(jù)集大小
/// </summary>
public int Count => imageFilePaths.Length; // 返回圖像文件數(shù)量
/// <summary>
/// 索引器 - 通過索引獲取單個數(shù)據(jù)樣本
/// </summary>
public Dictionary<string, Tensor> this[int index]
{
get
{
if (index < 0 || index >= imageFilePaths.Length) // 檢查索引有效性
throw new IndexOutOfRangeException($"索引 {index} 超出范圍 [0, {imageFilePaths.Length - 1}]");
return LoadSingleSample(index); // 加載單個樣本
}
}
/// <summary>
/// 加載單個樣本 - 讀取圖像和標注并執(zhí)行預(yù)處理
/// </summary>
private Dictionary<string, Tensor> LoadSingleSample(int index)
{
// 加載并預(yù)處理圖像
Tensor processedImage = LoadAndPreprocessImage(imageFilePaths[index]); // 加載和預(yù)處理圖像
// 加載并解析標注
Tensor processedAnnotations = LoadAndParseAnnotations(annotationFilePaths[index]); // 加載和解析標注
// 應(yīng)用數(shù)據(jù)增強(訓(xùn)練時)
if (config.EnableDarknessEnhancement) // 如果啟用數(shù)據(jù)增強
{
(processedImage, processedAnnotations) = ApplyTrainingAugmentations(processedImage, processedAnnotations); // 應(yīng)用數(shù)據(jù)增強
}
// 返回樣本字典
return new Dictionary<string, Tensor>
{
{ "image", processedImage }, // 處理后的圖像張量
{ "target", processedAnnotations } // 處理后的標注張量
};
}
/// <summary>
/// 加載和預(yù)處理圖像 - 讀取圖像文件并轉(zhuǎn)換為模型輸入格式
/// </summary>
private Tensor LoadAndPreprocessImage(string imagePath)
{
// 使用System.Drawing加載圖像
using (var bitmap = new Bitmap(imagePath)) // 加載位圖文件
{
// 轉(zhuǎn)換為RGB格式(確保3通道)
using (var rgbBitmap = new Bitmap(bitmap.Width, bitmap.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb)) // 創(chuàng)建RGB位圖
{
using (var graphics = Graphics.FromImage(rgbBitmap)) // 創(chuàng)建繪圖對象
{
graphics.DrawImage(bitmap, 0, 0, bitmap.Width, bitmap.Height); // 繪制原圖像
}
// 將Bitmap轉(zhuǎn)換為Tensor
Tensor imageTensor = BitmapToTensor(rgbBitmap); // 轉(zhuǎn)換位圖為張量
// 應(yīng)用圖像預(yù)處理
imageTensor = PreprocessImageTensor(imageTensor); // 預(yù)處理圖像張量
return imageTensor; // 返回處理后的張量
}
}
}
/// <summary>
/// 將Bitmap轉(zhuǎn)換為Tensor - 圖像數(shù)據(jù)轉(zhuǎn)換為PyTorch張量格式
/// </summary>
private Tensor BitmapToTensor(Bitmap bitmap)
{
var bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), // 鎖定位圖數(shù)據(jù)
System.Drawing.Imaging.ImageLockMode.ReadOnly, bitmap.PixelFormat); // 只讀模式
try
{
int bytesPerPixel = Image.GetPixelFormatSize(bitmap.PixelFormat) / 8; // 計算每像素字節(jié)數(shù)
byte[] pixelData = new byte[bitmapData.Stride * bitmap.Height]; // 創(chuàng)建像素數(shù)據(jù)數(shù)組
Marshal.Copy(bitmapData.Scan0, pixelData, 0, pixelData.Length); // 復(fù)制非托管數(shù)據(jù)到托管數(shù)組
// 將字節(jié)數(shù)據(jù)轉(zhuǎn)換為float張量
Tensor tensor = torch.zeros(new long[] { bitmap.Height, bitmap.Width, 3 }, torch.float32); // 創(chuàng)建空張量
for (int y = 0; y < bitmap.Height; y++) // 遍歷所有行
{
for (int x = 0; x < bitmap.Width; x++) // 遍歷所有列
{
int index = y * bitmapData.Stride + x * bytesPerPixel; // 計算像素索引
// 讀取BGR值并轉(zhuǎn)換為RGB
float b = pixelData[index] / 255.0f; // 藍色通道,歸一化到[0,1]
float g = pixelData[index + 1] / 255.0f; // 綠色通道,歸一化到[0,1]
float r = pixelData[index + 2] / 255.0f; // 紅色通道,歸一化到[0,1]
tensor[y, x, 0] = r; // 紅色通道
tensor[y, x, 1] = g; // 綠色通道
tensor[y, x, 2] = b; // 藍色通道
}
}
return tensor; // 返回圖像張量
}
finally
{
bitmap.UnlockBits(bitmapData); // 解鎖位圖數(shù)據(jù)
}
}
/// <summary>
/// 圖像預(yù)處理 - 調(diào)整尺寸、歸一化等操作
/// </summary>
private Tensor PreprocessImageTensor(Tensor image)
{
// 調(diào)整圖像尺寸到目標大小
image = functional.interpolate(image.unsqueeze(0), // 添加批次維度并插值
new long[] { inputImageSize, inputImageSize }, // 目標尺寸
mode: InterpolationMode.Bilinear, // 雙線性插值
align_corners: false).squeeze(0); // 移除批次維度
// 如果配置為單通道輸入,轉(zhuǎn)換為灰度圖
if (config.InputChannels == 1) // 檢查是否需要單通道
{
image = ConvertToGrayscale(image); // 轉(zhuǎn)換為灰度圖
}
// 歸一化到[0,1]范圍(如果尚未歸一化)
if (image.max().item<float>() > 1.0f) // 檢查是否已經(jīng)歸一化
{
image = image / 255.0f; // 歸一化到[0,1]
}
// 調(diào)整維度順序為 [C, H, W]
image = image.permute(new long[] { 2, 0, 1 }); // 從[H,W,C]變?yōu)閇C,H,W]
return image; // 返回預(yù)處理后的圖像
}
/// <summary>
/// 轉(zhuǎn)換為灰度圖 - 將RGB圖像轉(zhuǎn)換為單通道灰度圖
/// </summary>
private Tensor ConvertToGrayscale(Tensor rgbImage)
{
// 使用標準灰度轉(zhuǎn)換公式: Y = 0.299R + 0.587G + 0.114B
Tensor grayscale = 0.299f * rgbImage[":", ":", 0] + // 紅色分量
0.587f * rgbImage[":", ":", 1] + // 綠色分量
0.114f * rgbImage[":", ":", 2]; // 藍色分量
return grayscale.unsqueeze(2); // 添加通道維度 [H, W, 1]
}
/// <summary>
/// 加載和解析標注 - 讀取標注文件并轉(zhuǎn)換為模型目標格式
/// </summary>
private Tensor LoadAndParseAnnotations(string annotationPath)
{
var annotations = new List<float[]>(); // 創(chuàng)建標注列表
if (File.Exists(annotationPath)) // 檢查標注文件是否存在
{
string[] lines = File.ReadAllLines(annotationPath); // 讀取所有行
foreach (string line in lines) // 遍歷每一行
{
if (string.IsNullOrWhiteSpace(line)) // 跳過空行
continue;
string[] parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); // 分割字符串
if (parts.Length >= 5) // 檢查格式是否正確(class x_center y_center width height)
{
float classId = float.Parse(parts[0]); // 類別ID
float xCenter = float.Parse(parts[1]); // 中心點x坐標(歸一化)
float yCenter = float.Parse(parts[2]); // 中心點y坐標(歸一化)
float width = float.Parse(parts[3]); // 寬度(歸一化)
float height = float.Parse(parts[4]); // 高度(歸一化)
annotations.Add(new float[] { classId, xCenter, yCenter, width, height }); // 添加到列表
}
}
}
// 轉(zhuǎn)換為Tensor格式
if (annotations.Count > 0) // 如果有標注
{
Tensor annotationTensor = torch.zeros(new long[] { annotations.Count, 5 }, torch.float32); // 創(chuàng)建標注張量
for (int i = 0; i < annotations.Count; i++) // 遍歷所有標注
{
annotationTensor[i] = torch.tensor(annotations[i]); // 設(shè)置每一行數(shù)據(jù)
}
return annotationTensor; // 返回標注張量
}
else // 如果沒有標注(負樣本)
{
return torch.zeros(new long[] { 0, 5 }, torch.float32); // 返回空標注
}
}
/// <summary>
/// 應(yīng)用訓(xùn)練時數(shù)據(jù)增強 - 提高模型泛化能力
/// </summary>
private (Tensor image, Tensor annotations) ApplyTrainingAugmentations(Tensor image, Tensor annotations)
{
Tensor augmentedImage = image.clone(); // 克隆圖像,避免修改原始數(shù)據(jù)
Tensor augmentedAnnotations = annotations.clone(); // 克隆標注
// 隨機水平翻轉(zhuǎn)(50%概率)
if (config.EnableHorizontalFlip && randomGenerator.NextDouble() > 0.5) // 檢查是否啟用并隨機決定
{
(augmentedImage, augmentedAnnotations) = ApplyHorizontalFlip(augmentedImage, augmentedAnnotations); // 應(yīng)用水平翻轉(zhuǎn)
}
// 隨機亮度調(diào)整
if (randomGenerator.NextDouble() > 0.5) // 50%概率應(yīng)用亮度調(diào)整
{
augmentedImage = AdjustBrightness(augmentedImage, config.LuminanceAdjustment); // 調(diào)整亮度
}
// 隨機對比度調(diào)整
if (randomGenerator.NextDouble() > 0.5) // 50%概率應(yīng)用對比度調(diào)整
{
augmentedImage = AdjustContrast(augmentedImage, config.ContrastVariation); // 調(diào)整對比度
}
// 針對暗區(qū)域的特殊增強
if (config.EnableDarknessEnhancement) // 如果啟用暗區(qū)域增強
{
augmentedImage = EnhanceDarkRegions(augmentedImage); // 增強暗區(qū)域
}
return (augmentedImage, augmentedAnnotations); // 返回增強后的數(shù)據(jù)和標注
}
/// <summary>
/// 應(yīng)用水平翻轉(zhuǎn) - 同時翻轉(zhuǎn)圖像和調(diào)整標注坐標
/// </summary>
private (Tensor image, Tensor annotations) ApplyHorizontalFlip(Tensor image, Tensor annotations)
{
// 翻轉(zhuǎn)圖像(在寬度維度)
Tensor flippedImage = functional.pad(image, new long[] { 0, 0, 0, 0 }, mode: PaddingModes.Reflect); // 填充
flippedImage = torch.flip(flippedImage, new long[] { 2 }); // 沿寬度維度翻轉(zhuǎn)
// 調(diào)整標注坐標
if (annotations.shape[0] > 0) // 如果有標注
{
Tensor flippedAnnotations = annotations.clone(); // 克隆標注
flippedAnnotations[":", 1] = 1.0f - flippedAnnotations[":", 1]; // 翻轉(zhuǎn)x中心坐標
annotations = flippedAnnotations; // 更新標注
}
return (flippedImage, annotations); // 返回翻轉(zhuǎn)后的圖像和標注
}
/// <summary>
/// 調(diào)整亮度 - 隨機改變圖像亮度
/// </summary>
private Tensor AdjustBrightness(Tensor image, float maxAdjustment)
{
float adjustment = (float)(randomGenerator.NextDouble() * maxAdjustment * 2 - maxAdjustment); // 隨機亮度調(diào)整量
return torch.clamp(image + adjustment, 0.0f, 1.0f); // 應(yīng)用調(diào)整并限制范圍
}
/// <summary>
/// 調(diào)整對比度 - 隨機改變圖像對比度
/// </summary>
private Tensor AdjustContrast(Tensor image, float maxFactor)
{
float factor = (float)(1.0 + randomGenerator.NextDouble() * maxFactor * 2 - maxFactor); // 隨機對比度因子
Tensor mean = image.mean(); // 計算圖像均值
return torch.clamp((image - mean) * factor + mean, 0.0f, 1.0f); // 應(yīng)用對比度調(diào)整
}
/// <summary>
/// 增強暗區(qū)域 - 專門針對暗區(qū)域的對比度增強
/// </summary>
private Tensor EnhanceDarkRegions(Tensor image)
{
// 創(chuàng)建暗區(qū)域掩碼(像素值低于閾值)
Tensor darkMask = image < config.DarknessThreshold; // 暗區(qū)域掩碼
if (darkMask.any().item<bool>()) // 如果存在暗區(qū)域
{
// 增強暗區(qū)域?qū)Ρ榷?br />
Tensor enhancedDark = image * 1.5f; // 增強暗區(qū)域
enhancedDark = torch.clamp(enhancedDark, 0.0f, 1.0f); // 限制范圍
// 應(yīng)用掩碼:只增強暗區(qū)域
image = torch.where(darkMask, enhancedDark, image); // 條件替換
}
return image; // 返回增強后的圖像
}
/// <summary>
/// 實現(xiàn)迭代器接口 - 支持foreach遍歷
/// </summary>
public IEnumerator<Dictionary<string, Tensor>> GetEnumerator()
{
for (int i = 0; i < imageFilePaths.Length; i++) // 遍歷所有樣本
{
yield return this[i]; // 返回當(dāng)前樣本
}
}
/// <summary>
/// 顯式接口實現(xiàn) - 返回非泛型迭代器
/// </summary>
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator(); // 返回泛型迭代器
}
/// <summary>
/// 釋放資源 - 實現(xiàn)IDisposable接口
/// </summary>
public void Dispose()
{
if (!isDisposed) // 如果尚未釋放
{
// 這里可以釋放任何非托管資源
isDisposed = true; // 標記為已釋放
GC.SuppressFinalize(this); // 阻止終結(jié)器調(diào)用
}
}
}
2. 暗區(qū)域訓(xùn)練器(完整訓(xùn)練流程)
csharp
/// <summary>
/// 暗區(qū)域檢測訓(xùn)練器 - 管理完整的模型訓(xùn)練流程
/// 包含訓(xùn)練循環(huán)、驗證、模型保存和進度監(jiān)控
/// </summary>
public class DarkRegionTrainer : IDisposable
{
private DarkRegionDetector model; // 暗區(qū)域檢測模型
private optim.Optimizer modelOptimizer; // 模型優(yōu)化器
private DarkRegionDetectionLoss lossFunction; // 損失函數(shù)
private DarkRegionDetectorConfig trainingConfig; // 訓(xùn)練配置
private Device trainingDevice; // 訓(xùn)練設(shè)備(CPU/GPU)
private LearningRateScheduler learningRateScheduler; // 學(xué)習(xí)率調(diào)度器
private bool isDisposed = false; // 資源釋放標志
/// <summary>
/// 訓(xùn)練進度事件 - 用于報告訓(xùn)練進度和指標
/// </summary>
public event Action<TrainingProgress> TrainingProgressUpdated;
/// <summary>
/// 構(gòu)造函數(shù) - 初始化訓(xùn)練器的所有組件
/// </summary>
public DarkRegionTrainer(DarkRegionDetectorConfig config)
{
this.trainingConfig = config; // 保存訓(xùn)練配置
InitializeTrainingDevice(); // 初始化訓(xùn)練設(shè)備
InitializeModelComponents(); // 初始化模型和優(yōu)化器
InitializeLearningRateScheduler(); // 初始化學(xué)習(xí)率調(diào)度器
Console.WriteLine($"訓(xùn)練器初始化完成,使用設(shè)備: {trainingDevice}"); // 輸出初始化信息
}
/// <summary>
/// 初始化訓(xùn)練設(shè)備 - 自動選擇CPU或GPU
/// </summary>
private void InitializeTrainingDevice()
{
if (torch.cuda.is_available()) // 檢查CUDA是否可用
{
trainingDevice = Device.CUDA; // 使用GPU
Console.WriteLine("檢測到CUDA設(shè)備,使用GPU進行訓(xùn)練"); // 輸出GPU信息
}
else // 如果沒有GPU
{
trainingDevice = Device.CPU; // 使用CPU
Console.WriteLine("未檢測到CUDA設(shè)備,使用CPU進行訓(xùn)練"); // 輸出CPU信息
}
}
/// <summary>
/// 初始化模型組件 - 創(chuàng)建模型、損失函數(shù)和優(yōu)化器
/// </summary>
private void InitializeModelComponents()
{
// 初始化暗區(qū)域檢測模型
this.model = new DarkRegionDetector(trainingConfig, trainingDevice, ScalarType.Float32); // 創(chuàng)建模型
// 初始化損失函數(shù),針對暗區(qū)域檢測優(yōu)化參數(shù)
this.lossFunction = new DarkRegionDetectionLoss(
darkRegionWeight: 2.0f, // 暗區(qū)域權(quán)重較高
positiveSampleWeight: 1.0f, // 正樣本標準權(quán)重
negativeSampleWeight: 0.5f // 負樣本權(quán)重較低
);
// 將模型和損失函數(shù)移動到訓(xùn)練設(shè)備
model.to(trainingDevice); // 移動模型到設(shè)備
lossFunction.to(trainingDevice); // 移動損失函數(shù)到設(shè)備
// 初始化優(yōu)化器,使用Adam優(yōu)化器
var trainableParameters = model.parameters().Where(param => param.requires_grad).ToList(); // 獲取可訓(xùn)練參數(shù)
this.modelOptimizer = optim.Adam(
trainableParameters, // 可訓(xùn)練參數(shù)列表
trainingConfig.InitialLearningRate, // 初始學(xué)習(xí)率
weight_decay: trainingConfig.RegularizationStrength // 權(quán)重衰減
);
Console.WriteLine($"模型初始化完成,可訓(xùn)練參數(shù): {trainableParameters.Count}"); // 輸出模型信息
}
/// <summary>
/// 初始化學(xué)習(xí)率調(diào)度器 - 動態(tài)調(diào)整學(xué)習(xí)率
/// </summary>
private void InitializeLearningRateScheduler()
{
// 使用余弦退火學(xué)習(xí)率調(diào)度
this.learningRateScheduler = optim.lr_scheduler.CosineAnnealingLR(
modelOptimizer, // 優(yōu)化器
T_max: trainingConfig.TotalEpochs, // 總周期數(shù)
eta_min: trainingConfig.InitialLearningRate * 0.01f // 最小學(xué)習(xí)率
);
}
/// <summary>
/// 執(zhí)行完整訓(xùn)練流程 - 包含訓(xùn)練和驗證
/// </summary>
public void ExecuteTraining(string trainingImagesPath, string trainingAnnotationsPath,
string validationImagesPath = null, string validationAnnotationsPath = null)
{
// 加載訓(xùn)練數(shù)據(jù)集
using (var trainingDataset = new DarkRegionDataset(trainingImagesPath, trainingAnnotationsPath, trainingConfig)) // 創(chuàng)建訓(xùn)練數(shù)據(jù)集
{
DarkRegionDataset validationDataset = null; // 驗證數(shù)據(jù)集
// 如果有驗證數(shù)據(jù),加載驗證集
if (!string.IsNullOrEmpty(validationImagesPath) && !string.IsNullOrEmpty(validationAnnotationsPath)) // 檢查驗證路徑
{
validationDataset = new DarkRegionDataset(validationImagesPath, validationAnnotationsPath, trainingConfig); // 創(chuàng)建驗證數(shù)據(jù)集
Console.WriteLine($"驗證集加載完成: {validationDataset.Count} 個樣本"); // 輸出驗證集信息
}
// 創(chuàng)建數(shù)據(jù)加載器
using (var trainingDataLoader = new DataLoader(trainingDataset, trainingConfig.BatchSize, shuffle: true)) // 訓(xùn)練數(shù)據(jù)加載器
{
// 執(zhí)行訓(xùn)練循環(huán)
for (int currentEpoch = 0; currentEpoch < trainingConfig.TotalEpochs; currentEpoch++) // 遍歷所有訓(xùn)練周期
{
// 執(zhí)行單個訓(xùn)練周期
float epochLoss = ExecuteSingleTrainingEpoch(trainingDataLoader, currentEpoch); // 訓(xùn)練一個周期
// 如果有驗證集,執(zhí)行驗證
float validationLoss = 0f;
if (validationDataset != null) // 如果有驗證集
{
using (var validationDataLoader = new DataLoader(validationDataset, trainingConfig.BatchSize, shuffle: false)) // 驗證數(shù)據(jù)加載器
{
validationLoss = ExecuteValidationEpoch(validationDataLoader, currentEpoch); // 執(zhí)行驗證
}
}
// 更新學(xué)習(xí)率
learningRateScheduler.step(); // 調(diào)整學(xué)習(xí)率
// 報告訓(xùn)練進度
ReportTrainingProgress(currentEpoch, epochLoss, validationLoss); // 報告進度
// 定期保存模型檢查點
if ((currentEpoch + 1) % 10 == 0 || currentEpoch == trainingConfig.TotalEpochs - 1) // 每10個周期或最后周期
{
SaveModelCheckpoint(currentEpoch, epochLoss, validationLoss); // 保存檢查點
}
}
}
// 釋放驗證數(shù)據(jù)集
validationDataset?.Dispose(); // 如果存在驗證集,釋放資源
}
Console.WriteLine("訓(xùn)練完成!"); // 輸出完成信息
}
/// <summary>
/// 執(zhí)行單個訓(xùn)練周期 - 遍歷整個訓(xùn)練集并更新模型參數(shù)
/// </summary>
private float ExecuteSingleTrainingEpoch(DataLoader trainingLoader, int epochNumber)
{
model.train(); // 設(shè)置模型為訓(xùn)練模式
float totalEpochLoss = 0f; // 累計損失
int processedBatches = 0; // 已處理批次計數(shù)
Console.WriteLine($"開始訓(xùn)練周期 {epochNumber + 1}/{trainingConfig.TotalEpochs}"); // 輸出周期開始信息
foreach (var batchData in trainingLoader) // 遍歷所有訓(xùn)練批次
{
// 清空梯度
modelOptimizer.zero_grad(); // 清零梯度


粒機.jpg&w=689&h=388&a=&zc=1)



包裝機-4.png&w=689&h=388&a=&zc=1)


評論