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

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












評論