ML.NETで製造業の品質予測を劇的に改善した話 - ある開発チームの180日間の挑戦
ML.NETで製造業の品質予測を劇的に改善した話 - ある開発チームの180日間の挑戦
2025年6月27日
「このままだと、来月の納期に間に合わない...」
2024年の梅雨入り前、私たちのチームは重い空気に包まれていました。大手製造業のクライアントから依頼された品質検査の自動化プロジェクト。当初はPythonで開発を進めていましたが、既存の.NETシステムとの統合で大きな壁にぶつかっていたのです。
そんな時、一人のエンジニアが提案しました。「ML.NETを使ってみませんか?」
この決断が、プロジェクトの運命を大きく変えることになるとは、その時は誰も想像していませんでした。
プロジェクトの背景 - なぜ品質予測が必要だったのか
クライアントの課題
私たちのクライアントは、精密部品を製造する企業でした。彼らが抱えていた最大の課題は、製品の最終検査にかかる膨大なコストと時間でした。
【検査の現状】
- 1日あたり約10,000個の部品を製造
- 全数検査に熟練検査員15名が従事
- 検査時間:1個あたり平均3分
- 不良品率:約2.3%
- 見逃し率:0.5%(後工程で発覚)
特に深刻だったのは、熟練検査員の高齢化と人材不足。このままでは3年以内に検査体制が維持できなくなる可能性がありました。
最初の失敗 - Pythonアプローチの限界
当初、私たちはPythonとTensorFlowで解決しようとしました。確かに、モデルの精度は良好でした。しかし...
# 当初のPythonコード(抜粋)
def predict_quality(sensor_data):
# データの前処理
processed_data = preprocess(sensor_data)
# モデル予測
prediction = model.predict(processed_data)
# 結果をREST APIで返す
return jsonify({
'quality': 'OK' if prediction[0] > 0.95 else 'NG',
'confidence': float(prediction[0])
})
問題は、これを既存の.NET製造実行システム(MES)に統合することでした。
「APIのレスポンスが遅すぎる!生産ラインが止まってしまう」
現場からの悲鳴に近い声。1秒間に10個の部品が流れる生産ラインで、100ミリ秒以上の遅延は致命的でした。
転機 - ML.NETとの出会い
なぜML.NETを選んだのか
チームミーティングで、若手エンジニアのK君が提案しました。
「既存システムが全部.NETなら、ML.NETを使えばネイティブに統合できるんじゃないですか?」
正直、最初は半信半疑でした。「.NETで機械学習?本当に実用レベルなの?」
しかし、調査を進めるうちに、ML.NETの可能性が見えてきました:
- 既存.NETシステムとのシームレスな統合
- Pythonモデルの移行が可能(ONNX経由)
- C#による型安全な実装
- Windowsサービスとしての安定運用
プロトタイプ開発 - 最初の手応え
まず、小さなプロトタイプを作ってみることにしました。
// 最初のプロトタイプ
public class QualityPredictionService
{
private readonly MLContext _mlContext;
private ITransformer _model;
public QualityPredictionService()
{
_mlContext = new MLContext(seed: 1);
LoadModel();
}
public QualityPrediction Predict(SensorReading reading)
{
var predictionEngine = _mlContext.Model
.CreatePredictionEngine<SensorReading, QualityPrediction>(_model);
return predictionEngine.Predict(reading);
}
}
驚いたことに、レスポンスタイムは平均15ミリ秒。Pythonの1/10以下でした!
本格的な開発 - 数々の挑戦
データの前処理 - 現場のノイズとの戦い
実際の製造現場のデータは、想像以上にノイジーでした。センサーの異常値、欠損、時系列のズレ...
public class SensorDataPreprocessor
{
private readonly ILogger<SensorDataPreprocessor> _logger;
private readonly RunningStatistics _stats;
public ProcessedSensorData Process(RawSensorData raw)
{
// 異常値の検出と補正
var temperature = raw.Temperature;
if (temperature < -50 || temperature > 200)
{
_logger.LogWarning("異常な温度値を検出: {Temp}°C", temperature);
temperature = _stats.GetMedianTemperature();
}
// 振動データのフーリエ変換
var vibrationFeatures = ExtractVibrationFeatures(raw.VibrationData);
// 時系列特徴量の抽出
var timeSeriesFeatures = new[]
{
CalculateMovingAverage(raw.Pressure, 10),
CalculateStandardDeviation(raw.Pressure, 10),
DetectTrend(raw.Pressure)
};
return new ProcessedSensorData
{
Temperature = temperature,
Pressure = raw.Pressure,
VibrationIntensity = vibrationFeatures.Intensity,
VibrationFrequency = vibrationFeatures.DominantFrequency,
TimeSeriesFeatures = timeSeriesFeatures
};
}
}
特徴エンジニアリング - ドメイン知識の重要性
ここで大きな助けとなったのが、現場の熟練検査員の方々でした。
「不良品が出るときは、必ず圧力が一瞬下がるんだよ」 「振動のパターンが変わると、その後の10個は要注意だね」
こうした現場の知見を、特徴量として組み込んでいきました:
public class DomainSpecificFeatureExtractor
{
// 熟練検査員の知見を特徴量化
public float[] ExtractExpertFeatures(SensorDataWindow window)
{
var features = new List<float>();
// 圧力の急激な変化を検出
var pressureDrops = DetectPressureDrops(window.PressureReadings);
features.Add(pressureDrops.Count);
features.Add(pressureDrops.Any() ? pressureDrops.Max() : 0);
// 振動パターンの変化点
var vibrationChangePoints = DetectChangePoints(window.VibrationData);
features.Add(vibrationChangePoints.Count);
// 温度と圧力の相関
var tempPressureCorrelation = CalculateCorrelation(
window.TemperatureReadings,
window.PressureReadings);
features.Add(tempPressureCorrelation);
// 過去の不良品発生パターンとの類似度
var similarityScore = CalculatePatternSimilarity(window);
features.Add(similarityScore);
return features.ToArray();
}
}
モデルのトレーニング - 試行錯誤の日々
ML.NETのAutoMLを使って、最適なアルゴリズムを探索しました:
public async Task<ModelTrainingResult> TrainModelAsync(string dataPath)
{
var experimentTime = TimeSpan.FromHours(2);
_logger.LogInformation("AutoML実験を開始します。予定時間: {Time}", experimentTime);
// データの読み込み
var dataView = _mlContext.Data.LoadFromTextFile<QualityData>(
dataPath, hasHeader: true, separatorChar: ',');
// 不均衡データへの対処
var balancedData = BalanceDataset(dataView);
// AutoML実験の設定
var experimentSettings = new BinaryClassificationExperimentSettings
{
MaxExperimentTimeInSeconds = (uint)experimentTime.TotalSeconds,
OptimizingMetric = BinaryClassificationMetric.AreaUnderPrecisionRecallCurve,
CacheDirectoryName = "MLCache"
};
// プログレスハンドラー
var progress = new Progress<RunDetail<BinaryClassificationMetrics>>(detail =>
{
if (detail.ValidationMetrics != null)
{
_logger.LogInformation(
"[{Trainer}] AUC-PR: {AucPr:F4}, 精度: {Accuracy:F4}, 再現率: {Recall:F4}",
detail.TrainerName,
detail.ValidationMetrics.AreaUnderPrecisionRecallCurve,
detail.ValidationMetrics.Accuracy,
detail.ValidationMetrics.Recall);
}
});
// 実験の実行
var experiment = _mlContext.Auto()
.CreateBinaryClassificationExperiment(experimentSettings);
var result = await Task.Run(() =>
experiment.Execute(balancedData, "Label", progressHandler: progress));
_logger.LogInformation(
"最良モデル: {Model}, AUC-PR: {Score:F4}",
result.BestRun.TrainerName,
result.BestRun.ValidationMetrics.AreaUnderPrecisionRecallCurve);
return new ModelTrainingResult
{
Model = result.BestRun.Model,
Metrics = result.BestRun.ValidationMetrics,
TrainerName = result.BestRun.TrainerName
};
}
結果は予想以上でした:
- 精度: 98.7%
- 再現率: 96.2%(不良品の見逃しが激減)
- 適合率: 94.8%
リアルタイム処理 - 生産ラインとの統合
最大の課題は、高速で流れる生産ラインに対応することでした:
public class RealTimeQualityMonitor
{
private readonly Channel<SensorReading> _readingChannel;
private readonly ITransformer _model;
private readonly MLContext _mlContext;
private readonly IProductionLineController _lineController;
public RealTimeQualityMonitor()
{
_readingChannel = Channel.CreateUnbounded<SensorReading>();
StartProcessing();
}
private void StartProcessing()
{
// バッチ処理による高速化
Task.Run(async () =>
{
var batch = new List<SensorReading>(32);
var batchTimer = new Timer(_ => ProcessBatch(), null,
TimeSpan.FromMilliseconds(10), TimeSpan.FromMilliseconds(10));
await foreach (var reading in _readingChannel.Reader.ReadAllAsync())
{
batch.Add(reading);
if (batch.Count >= 32)
{
ProcessBatch();
}
}
void ProcessBatch()
{
if (batch.Count == 0) return;
var stopwatch = Stopwatch.StartNew();
var currentBatch = batch.ToList();
batch.Clear();
// バッチ予測
var dataView = _mlContext.Data.LoadFromEnumerable(currentBatch);
var predictions = _model.Transform(dataView);
var results = _mlContext.Data
.CreateEnumerable<QualityPrediction>(predictions, false)
.ToList();
// 不良品検出時の即座の対応
for (int i = 0; i < results.Count; i++)
{
if (results[i].PredictedLabel && results[i].Probability > 0.95)
{
_lineController.MarkDefective(currentBatch[i].ProductId);
_logger.LogWarning(
"不良品を検出: ProductId={Id}, 確率={Prob:P}",
currentBatch[i].ProductId, results[i].Probability);
}
}
_telemetry.TrackMetric("BatchProcessingTime",
stopwatch.ElapsedMilliseconds);
}
});
}
}
本番環境への移行 - 緊張の瞬間
段階的なロールアウト
いきなり全ラインに適用するのはリスクが高すぎました。そこで、段階的な導入戦略を取りました:
public class PhaseRolloutManager
{
private readonly Dictionary<string, RolloutPhase> _phases = new()
{
["Phase1"] = new RolloutPhase
{
Name = "パイロット運用",
Lines = new[] { "Line-A" },
TrafficPercentage = 10,
Duration = TimeSpan.FromDays(7),
SuccessCriteria = new SuccessCriteria
{
MinAccuracy = 0.95,
MaxFalsePositiveRate = 0.05,
MaxLatencyMs = 50
}
},
["Phase2"] = new RolloutPhase
{
Name = "限定展開",
Lines = new[] { "Line-A", "Line-B", "Line-C" },
TrafficPercentage = 50,
Duration = TimeSpan.FromDays(14)
},
["Phase3"] = new RolloutPhase
{
Name = "全面展開",
Lines = new[] { "All" },
TrafficPercentage = 100,
Duration = TimeSpan.MaxValue
}
};
public async Task<bool> AdvanceToNextPhaseAsync(string currentPhase)
{
var metrics = await _metricsCollector.GetPhaseMetricsAsync(currentPhase);
var phase = _phases[currentPhase];
// 成功基準のチェック
if (metrics.Accuracy < phase.SuccessCriteria.MinAccuracy)
{
_logger.LogWarning(
"精度が基準を下回っています: {Actual:P} < {Required:P}",
metrics.Accuracy, phase.SuccessCriteria.MinAccuracy);
return false;
}
if (metrics.FalsePositiveRate > phase.SuccessCriteria.MaxFalsePositiveRate)
{
_logger.LogWarning(
"誤検出率が基準を上回っています: {Actual:P} > {Required:P}",
metrics.FalsePositiveRate, phase.SuccessCriteria.MaxFalsePositiveRate);
return false;
}
_logger.LogInformation("フェーズ {Phase} の成功基準を満たしました", currentPhase);
return true;
}
}
最初のトラブル - そして解決
本番環境での初日、予期しないトラブルが発生しました。
「システムが頻繁に誤検出している!」
調査の結果、開発環境と本番環境でセンサーの校正値が異なることが判明。急遽、動的な校正機能を追加しました:
public class AdaptiveCalibration
{
private readonly ConcurrentDictionary<string, CalibrationParameters> _calibrations;
public async Task CalibrateAsync(string sensorId)
{
_logger.LogInformation("センサー {SensorId} の自動校正を開始", sensorId);
// 過去1時間の正常品データを取得
var normalData = await _dataStore.GetNormalProductDataAsync(
sensorId, TimeSpan.FromHours(1));
if (normalData.Count < 100)
{
_logger.LogWarning("校正に必要なデータが不足しています");
return;
}
// 統計的な校正パラメータの計算
var calibration = new CalibrationParameters
{
TemperatureOffset = CalculateMedian(normalData.Select(d => d.Temperature)) - 25.0f,
PressureScale = 1.0f / CalculateStandardDeviation(normalData.Select(d => d.Pressure)),
VibrationBaseline = CalculatePercentile(normalData.Select(d => d.Vibration), 10),
UpdatedAt = DateTime.UtcNow
};
_calibrations[sensorId] = calibration;
_logger.LogInformation(
"校正完了 - 温度オフセット: {TempOffset:F2}°C, 圧力スケール: {PressureScale:F3}",
calibration.TemperatureOffset, calibration.PressureScale);
}
public SensorReading ApplyCalibration(RawSensorReading raw)
{
if (!_calibrations.TryGetValue(raw.SensorId, out var calibration))
{
return raw.ToDefault();
}
return new SensorReading
{
Temperature = raw.Temperature - calibration.TemperatureOffset,
Pressure = raw.Pressure * calibration.PressureScale,
Vibration = Math.Max(0, raw.Vibration - calibration.VibrationBaseline),
Timestamp = raw.Timestamp
};
}
}
成果 - 数字が物語る成功
導入から3ヶ月後の成果
プロジェクト開始から180日。ついに全ラインでの本格運用が始まりました。その成果は:
【ビフォー】
- 検査員: 15名
- 検査時間: 3分/個
- 検査コスト: 年間1.2億円
- 不良品見逃し率: 0.5%
- 顧客クレーム: 月平均8件
【アフター】
- 検査員: 5名(確認作業のみ)
- 検査時間: 0.1秒/個(自動)
- 検査コスト: 年間3,600万円(70%削減)
- 不良品見逃し率: 0.08%(84%改善)
- 顧客クレーム: 月平均1件(87.5%削減)
現場の声
最も嬉しかったのは、現場の方々からの声でした:
「最初は機械に仕事を奪われると思っていたけど、実際は単純作業から解放されて、もっと重要な改善活動に時間を使えるようになった」(検査課 Aさん)
「不良品の見逃しが激減して、顧客からの信頼が向上した。営業としても自信を持って製品を売れる」(営業部 Bさん)
継続的な改善
運用開始後も、モデルの継続的な改善を続けています:
public class ContinuousLearningService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
// 毎週日曜日の深夜に再学習
var nextSunday = GetNextSunday();
var delay = nextSunday - DateTime.Now;
await Task.Delay(delay, stoppingToken);
_logger.LogInformation("週次の再学習を開始します");
// 過去1週間のデータを収集
var trainingData = await CollectWeeklyDataAsync();
// データの品質チェック
if (!await ValidateDataQualityAsync(trainingData))
{
_logger.LogWarning("データ品質が基準を満たしていません。再学習をスキップします");
continue;
}
// 新しいモデルの学習
var newModel = await TrainModelAsync(trainingData);
// A/Bテストの設定
await _abTestManager.SetupTestAsync(new ABTestConfig
{
TestName = $"WeeklyUpdate_{DateTime.Now:yyyyMMdd}",
ControlModel = _currentModel,
TestModel = newModel,
TrafficSplit = 10, // 10%のトラフィックで新モデルをテスト
Duration = TimeSpan.FromDays(3),
SuccessMetric = "F1Score"
});
_logger.LogInformation("新しいモデルのA/Bテストを開始しました");
}
catch (Exception ex)
{
_logger.LogError(ex, "再学習プロセスでエラーが発生しました");
}
}
}
}
学んだ教訓
1. 技術選定の重要性
最初からML.NETを選んでいれば、2ヶ月は短縮できたかもしれません。既存システムとの親和性は、想像以上に重要でした。
2. 現場の知見は宝物
機械学習エンジニアだけでは、この成果は出せませんでした。現場の熟練者の知識を特徴量に落とし込むことが、高精度の鍵でした。
3. 段階的導入の大切さ
いきなり全面導入していたら、初日のトラブルで大混乱になっていたでしょう。小さく始めて、徐々に拡大する戦略が功を奏しました。
4. 継続的な改善体制
モデルは作って終わりではありません。継続的な監視と改善の仕組みが、長期的な成功には不可欠です。
エピローグ - そして新たな挑戦へ
プロジェクト完了後、クライアントから新たな相談を受けました。
「この成功を、他の工程にも展開したい。次は組立工程の最適化をお願いできますか?」
ML.NETで培った知見を活かし、私たちは新たな挑戦に向けて歩み始めました。
技術仕様(参考)
このプロジェクトで使用した主な技術:
- ML.NET 3.0.1
- .NET 8.0
- Windows Server 2022
- SQL Server 2022
- Azure Application Insights(モニタリング)
- SignalR(リアルタイム通知)
主なNuGetパッケージ:
<PackageReference Include="Microsoft.ML" Version="3.0.1" />
<PackageReference Include="Microsoft.ML.AutoML" Version="0.21.1" />
<PackageReference Include="Microsoft.ML.TimeSeries" Version="3.0.1" />
<PackageReference Include="Microsoft.Extensions.ML" Version="3.0.1" />
さいごに
この記事を読んでくださった皆様の中にも、似たような課題を抱えている方がいらっしゃるかもしれません。ML.NETは、.NET環境で機械学習を実装する強力な選択肢です。
私たちの経験が、少しでも皆様のプロジェクトの参考になれば幸いです。
エンハンスド株式会社では、ML.NETを活用した機械学習システムの開発・導入支援を行っています。製造業に限らず、様々な業界での課題解決をサポートいたします。お気軽にご相談ください。