在医学图像处理这个方向里“皮肤癌检测”算是一个看起来简单、做起来全是细节的题目。我第一次接触这个项目是因为朋友手上的一个医学图像处理Demo需要快速搭一个早期筛查原型输入一批皮肤镜照片输出每个病灶是良性还是恶性。当时他手头只有几百张标注好的图片也没有GPU深度学习方案直接劝退。权衡之后我选了支持向量机SVM加上手工图像特征在Matlab里跑通了整个流程实测效果比预想中稳很多。这套系统的完整链路是图像预处理 → 病灶区域分割 → 特征提取 → SVM训练与调参 → 模型评估。坦白说网上搜“皮肤癌检测 支持向量机 Matlab”能出来一大堆同名项目但大多数只丢一段能跑的代码不讲为什么这样设计也不说坑在哪里。这篇博文把关键的技术选型逻辑、Matlab实现细节、以及我实际踩过的坑一次性讲清楚。适合正在做图像处理课程设计、毕业论文或者想快速验证医学图像分类想法的读者一台普通电脑加Matlab就能复现。1. 为什么皮肤癌检测会选SVM小样本、可解释与快速迭代1.1 临床筛查场景决定了系统定位皮肤癌里最危险的是恶性黑色素瘤它也是少数几种“长得越早、看得越准、活得越久”的癌症之一。皮肤科医生通过皮肤镜观察病灶的形态、颜色、边界经验丰富的人能凭肉眼给出很高的判断准确率但问题在于医生数量有限不可能给所有高危人群都做定期筛查。计算机辅助筛查系统想解决的就是这件事把皮肤镜图像自动分成“需要进一步检查”和“大概率没事”两类帮医生把精力集中在真正可疑的病例上。这里有一个非常重要的定位问题这个系统不是要取代医生而是做预筛。所以模型输出的结果无论多自信到最后都需要临床确认。这决定了我们在设计时不能盲目追求“分类准确率”而要把“漏掉恶性样本”当成最不能接受的事。后面讲评估指标的时候我会反复强调这一点。1.2 和深度学习比SVM在小数据集上的优势更实在很多初学者一听到图像分类就想到卷积神经网络但实际上手才发现问题一堆标注数据不够、显卡不够、训练时间太长、调参全靠玄学。我这次用的数据集规模只有几百张皮肤镜照片恶性样本可能只有一两百张。这点数据喂给CNN除非用很深的预训练模型做迁移学习否则很容易过拟合。SVM在这种“样本量少、特征维度中等”的场景下反而特别能打最大间隔分类器有比较强的泛化能力不像某些模型在小样本上稍微一训练就记住训练集。通过核函数可以处理非线性边界RBF核在多数医学图像特征上表现稳定。训练速度快普通CPU上几百个样本的训练时间是秒级网格搜索几十组参数也就是几分钟的事。模型本身是一个决策超平面配合特征重要性分析比深度模型的黑盒更容易向医生解释。另外如果你后续要写论文或者做毕设SVM作为基线方法几乎是必备对比项。就算最后用深度学习也得先有一个传统方法的benchmark这套流程可以原样复用。1.3 这个系统的边界与适合谁看需要先说清楚系统边界免得有人拿着它当临床工具。本系统的输入是单张皮肤镜图像输出是“良性/恶性”二分类结果。它不做病理切片分析不做病灶类型的细分类如黑色素瘤、基底细胞癌、脂溢性角化病等多分类也不处理视频或者时序数据。对单张静态图像来说临床上经典的“E”演化即病灶随时间变化特征是拿不到的这部分信息天然缺失。这篇内容适合三类读者一是做图像处理或模式识别课程项目的学生需要一个完整且能落地的案例二是毕业论文方向是医学图像分析的同学需要快速建立一个可复现的基线系统三是想验证“传统图像特征SVM”在某个小规模医学数据集上是否可行的工程师。如果你手里有大规模数据并且追求极限精度那该去折腾深度模型但建议还是把本文的SVM结果跑出来当参照。2. 数据准备与病灶分割毛发、反光和Otsu阈值2.1 公开皮肤镜数据集的基本情况这类项目通常用的是学术社区里公开的皮肤镜图像数据集一般包含几百到几千张病灶照片每张有对应的类别标注良性痣/恶性黑色素瘤部分数据集还提供医生手工勾画的病灶区域掩码。公开数据虽然方便但有几个共性特点要心里有数。第一图像来源不统一不同设备的色温、分辨率、拍摄角度都有差异第二病灶周围经常有毛发、血管、油渍、反光点甚至还有皮肤镜自带的刻度尺第三类别极不平衡良性样本通常比恶性样本多好几倍如果直接拿原始数据训练模型会偷懒。拿到数据后我习惯先做一次可视化统计把每张图的尺寸分布、两类样本数量、光照差异看一遍再决定预处理策略。顺便提一句如果是在医院合作项目里自己收集数据务必走正规的伦理审批和知情同意流程图像脱敏后再使用。自己收集的数据反而容易遇到病灶标注标准不统一的问题最好请两位以上皮肤科医生独立标注并交叉核对。2.2 预处理到底在解决什么皮肤镜图像里最影响特征提取的干扰源有四类毛发、反光、光照不均、背景皮肤。毛发在图像里是细长的亮线或暗线会参与纹理特征计算让灰度共生矩阵统计出大量虚假的“方向性纹理”反光点是高亮小圆斑在颜色特征里会拉高均值光照不均会让同一张图不同区域的亮度差异很大而背景皮肤如果没被分割掉颜色统计会被大面积正常皮肤主导病灶本身的信息反而被稀释。预处理和分割的目标就是把特征计算的注意力锁在病灶区域上。这一步做崩了后面再好的特征和分类器都白搭。我见过有人跳过预处理直接提特征毛发和反光被当成纹理之后分类准确率掉五到十个点都是正常的。2.3 去毛发、对比度增强与病灶分割的Matlab实现我用的预处理流程如下每一步的意图都写在注释里。这个函数输入图片路径输出分割掩码和清理后的灰度图function [lesionMask, cleanImg] preprocessLesion(imgPath) img imread(imgPath); % 统一尺寸256x256在保留细节和计算速度之间比较平衡 img imresize(img, [256 256]); gray rgb2gray(img); % 去毛发顶帽变换提取比周围更亮/更暗的细长结构 se strel(disk, 5); hair imtophat(gray, se); clean gray - hair; % 对比度拉伸增强病灶与皮肤的边界 clean imadjust(clean); % Otsu方法自动求阈值并二值化 level graythresh(clean); mask imbinarize(clean, level); % 形态学清理填洞、去小噪点、闭运算平滑边缘 mask imfill(mask, holes); mask bwareaopen(mask, 100); mask imclose(mask, strel(disk, 5)); lesionMask mask; cleanImg clean; end顶帽变换取的是形态学开运算结果和原图的差专门用来提取那些“细且亮”的结构。毛发在灰度图里通常是局部亮线所以能被分离出来减掉之后皮肤纹理就干净很多。直径5的圆盘结构元素是经验值太大了会把病灶内的亮色结构误伤太小了又盖不住粗毛发需要根据图像分辨率微调。分割这一步如果病灶和背景对比度强Otsu自动阈值就够了。如果效果不好可以改用HSV颜色空间的饱和度通道或者RGB的蓝色通道再来一次阈值分割很多时候比灰度图更稳。分割之后的掩码经常带有锯齿边缘我用的是先填充孔洞、再删除小面积连通域、最后闭运算的操作。提示病灶区域如果只占整张图的很小一部分阈值分割很容易把背景皮肤判成前景。这时候可以考虑先把图像裁剪到病灶附近或者用自适应阈值把图像分成子块分别取阈值。我实测下来配合后面特征提取时只统计掩码内的像素大部分情况Otsu是够用的。3. 特征工程把ABCDE临床规则翻译成特征向量3.1 临床规则到计算特征的映射皮肤科医生诊断黑色素瘤时有一套经典的ABCDE规则这套规则本质上就是人肉特征工程非常适合转化成图像特征。这也是我选择手工特征而不是直接端到端学习的原因之一特征能溯源到临床经验讲得清道理。临床特征含义可计算的替代特征A 不对称性病灶两半不对称主轴翻转重叠率、边界框长宽比、离心率B 边界边缘不规则、有锯齿圆度指数、边缘凹凸度、凸包缺陷面积C 颜色多种颜色混杂RGB/HSV各通道均值与标准差、颜色聚类数D 直径病灶直径偏大等效圆直径、边界框对角线长度E 演化短期内有变化单张静态图无法计算需随访图像这张表是整个特征工程的地图。每一行代表一类特征提取特征时的目标就是用数字复现医生肉眼看到的东西。注意E在单张图像里拿不到但如果数据集里有同一病灶不同时期的图像是可以做的这就属于扩展方向了。3.2 几何特征与颜色特征的提取几何特征我用Matlab的regionprops直接算一行代码能拿到一大堆属性。关键要理解每个特征对应的临床含义。function geoFeat extractGeometric(mask) stats regionprops(mask, Area, Perimeter, ... BoundingBox, Eccentricity); area stats.Area; perimeter stats.Perimeter; % 圆度指数圆形为1越不规则越大 circularity perimeter^2 / (4 * pi * area); % 等效直径把面积换算成圆的直径 eqDiameter 2 * sqrt(area / pi); % 边界框长宽比衡量整体对称性 bb stats.BoundingBox; % [x y w h] aspectRatio bb(3) / bb(4); geoFeat [circularity, eqDiameter, aspectRatio, stats.Eccentricity]; end边界不规则和不对称这两项是黑色素瘤的强信号所以圆度指数和长宽比是我永远保留的特征。颜色特征方面在掩码内统计RGB和HSV各通道的均值与标准差能比较好地刻画“颜色混杂”这件事。恶性病灶经常出现粉色、白色、蓝黑色多种色调混在一起颜色标准差会比普通痣大不少。function colorFeat extractColor(img, mask) % 掩码覆盖到每个通道 r img(:,:,1); g img(:,:,2); b img(:,:,3); hsvImg rgb2hsv(img); h hsvImg(:,:,1); s hsvImg(:,:,2); v hsvImg(:,:,3); chanList {r, g, b, h, s, v}; colorFeat []; for i 1:length(chanList) vals chanList{i}(mask); colorFeat [colorFeat, mean(vals), std(vals)]; end end3.3 GLCM纹理特征为什么要算灰度共生矩阵颜色和几何特征能覆盖ABCD里的三项但“纹理”是另一个重要的区分维度。普通痣的色素网络通常是均匀细密的而恶性病灶的色素网络往往粗糙、断裂、不对称。灰度共生矩阵GLCM统计的是图像中特定灰度对在某个方向和距离上共同出现的频率能从统计层面把“纹理粗不粗、均不均匀、有没有方向性”量化出来。我一般计算0度、45度、90度、135度四个方向上的GLCM每个方向提取对比度、相关性、能量、同质性四个统计量然后取四个方向上的均值和标准差作为特征。对比度反映纹理的沟纹深浅能量反映纹理的均匀程度这些对区分良恶性都很有用。function texFeat extractTexture(gray, mask) % 只取病灶边界框区域减少背景干扰 stats regionprops(mask, BoundingBox); bb ceil(stats.BoundingBox); cropGray imcrop(gray, bb); cropMask imcrop(mask, bb); % 掩码外像素置为0后再算GLCM cropGray(~cropMask) 0; offsets [0 1; -1 1; -1 0; -1 -1]; glcms graycomatrix(cropGray, Offset, offsets, ... NumLevels, 32, Symmetric, true); statsGLCM graycoprops(glcms, ... {Contrast, Correlation, Energy, Homogeneity}); texFeat [mean(statsGLCM.Contrast), std(statsGLCM.Contrast), ... mean(statsGLCM.Correlation), std(statsGLCM.Correlation), ... mean(statsGLCM.Energy), std(statsGLCM.Energy), ... mean(statsGLCM.Homogeneity), std(statsGLCM.Homogeneity)]; end为什么先裁剪到边界框再计算因为GLCM统计的是整张图的灰度分布如果背景皮肤占比太大纹理特征会被大面积正常皮肤稀释。把背景像素置为0之后灰度0会成为最大的灰度级之一所以裁剪到病灶附近再算这一招能明显提升纹理特征的判别力。3.4 特征组装、归一化与特征选择把所有特征拼起来我通常得到30到50维的向量几何4维、颜色12维、纹理8维还可以加入颜色直方图分箱特征、局部二值模式LBP直方图特征。特征不是越多越好在几百张样本的条件下维度太多会让SVM在训练集上表现得很好、在测试集上打回原形。归一化是必须做的一步。颜色特征的取值范围是0到255而圆度指数可能只有1到10如果不归一化RBF核计算距离时大数值特征会完全压制小数值特征。Matlab里最简单的做法是在fitcsvm中设置Standardize, true让模型在训练时自动记录均值和标准差预测时用同一套统计量归一化。如果自己手动zscore务必注意只统计训练集的均值和方差测试集要用训练集的统计量去换算否则就是数据泄漏。特征选择方面对小数据集我推荐最朴素的过滤法对每个特征单独计算它与标签的AUC或者做单因素ANOVA看p值然后保留区分度最高的10到20个特征。这个方法计算量小、不容易过拟合还能顺手观察每个特征单独能打多少分。别一上来就用复杂的迭代特征选择算法在样本少的时候容易选出“虚假的强特征”。4. SVM训练与调参RBF核、网格搜索与类别不平衡4.1 RBF核到底在做什么SVM的核心思想是找一个“间隔最大化”的分类超平面。但医学图像特征往往不是线性可分的比如良性和恶性样本在特征空间里可能是犬牙交错的状态。RBF核做的事情是把原始特征映射到一个更高维的空间在这个新空间里找一个更优的分隔面。通俗点说低维空间里绕来绕去的边界映射到高维之后可能就是一个平面的事。RBF核有两个关键参数在Matlab里分别叫BoxConstraint和KernelScale。BoxConstraint对应的是误分类惩罚权重值越大模型越不愿意放过训练集里的错误样本代价是可能过拟合值越小模型更宽容追求更大的间隔但可能欠拟合。KernelScale控制的是核函数的宽度它决定了样本之间的“相互作用半径”。核宽度太小每个样本只影响自己周围极小范围决策边界非常曲折过拟合核宽度太大所有样本都糊在一起模型退化成近似线性欠拟合。这两参数没有万能的固定值必须结合数据规模、特征分布来调。常见做法是对数网格搜索加交叉验证这也是我接下来要说的重点。4.2 网格搜索C和KernelScale的完整流程我习惯用10折分层交叉验证来选参。所谓分层就是每一折里良性和恶性的比例尽量和整体一致防止某折里全是良性样本导致评估失真。选择参数的标准我推荐用AUC因为AUC对分类阈值不敏感能更稳定地反映排序能力。% 假设X是特征矩阵labels是类别标签categorical Y double(labels malignant); % 1为恶性0为良性 rng(42); cv cvpartition(Y, KFold, 10); CList [0.1 1 10 100]; scaleList [0.01 0.1 1 10]; bestAUC 0; for c CList for s scaleList scoreAll zeros(size(X, 1), 1); predAll zeros(size(X, 1), 1); for k 1:cv.NumTestSets trIdx training(cv, k); teIdx test(cv, k); mdl fitcsvm(X(trIdx, :), Y(trIdx), ... KernelFunction, rbf, ... BoxConstraint, c, ... KernelScale, s, ... Standardize, true); [predAll(teIdx), scoreAll(teIdx)] predict(mdl, X(teIdx, :)); end [~, ~, ~, auc] perfcurve(Y, scoreAll, 1); if auc bestAUC bestAUC auc; bestC c; bestScale s; end end end fprintf(Best AUC%.4f, C%.2f, KernelScale%.2f\n, ... bestAUC, bestC, bestScale);这里有一个细节值得说明我把每个折的测试集预测分数收集起来然后再统一算AUC而不是每个折单独算AUC再取平均。这样得到的是“袋外分数”的AUC样本量大估计更稳。fitcsvm返回的score是带符号的决策距离数值本身不直接等于概率但perfcurve只需要排序能力所以用这个分数算AUC没有问题。Matlab也提供了OptimizeHyperparameters, {BoxConstraint,KernelScale}这种自动调参选项内部也是做贝叶斯优化。我建议第一次实验还是自己写网格搜索因为能清楚地看到参数和大小的关系等确定了大致区间再考虑用自动调参去微调。4.3 良性/恶性样本不平衡怎么办皮肤癌数据集几乎必然不平衡。我这里的数据良性恶性比例大约4比1极端情况下可能到10比1。如果不做任何处理SVM会学到一个几乎全预测为良性的模型准确率看起来高达80%甚至90%但把恶性样本全部漏掉。这种模型放在筛查场景里是灾难。最直接的办法是过采样少数类把恶性样本复制几份或者对恶性样本做旋转、翻转、小幅平移和亮度扰动来扩充。注意扩充必须放在训练集内部做不能把测试集也扩进去。另一个办法是调整错分代价在fitcsvm里通过Cost参数让“漏掉恶性”的代价远高于“误报良性”。我在实践中发现先用分层抽样保证每一折的类别比例一致再配合AUC选参比单纯调Cost矩阵更省事。如果数据实在太少还可以考虑合成少数类样本的经典做法但这类方法要谨慎使用因为合成的样本位置如果落在真实数据分布之外反而会把决策边界带偏。4.4 评估指标的选取别只盯着准确率医学筛查场景里准确率是最容易骗人的指标。分类器全预测良性准确率可能很高但毫无临床价值。真正要看的是敏感度和特异度。指标公式说明准确率 Accuracy(TPTN)/(TPTNFPFN)全局正确率不平衡时可能失真敏感度 SensitivityTP/(TPFN)恶性样本被正确检出的比例宁高勿低特异度 SpecificityTN/(TNFP)良性样本被正确判断的比例阳性预测值 PrecisionTP/(TPFP)检出结果为恶性的人中真正恶性比例F1分数2PR/(PR)精度和召回的综合筛查系统里敏感度是第一位。漏掉一个恶性样本可能延误治疗而多报几个良性样本最多是让患者多做一次复查。所以调参时如果AUC相近我会优先选敏感度更高的那组参数。模型训练完成后把测试集预测结果输出成混淆矩阵逐类看错在哪比只看一个准确率数字有用得多。5. Matlab代码结构从数据读取到模型部署5.1 系统整体调用链整个系统的代码组织可以分成五层每层职责单一方便单独调试和复用数据读取层用imageDatastore按文件夹自动生成标签预处理层调preprocessLesion完成去毛发、分割特征提取层调几何、颜色、纹理特征函数拼成特征向量训练调参层网格搜索加交叉验证确定最优超参数评估部署层测试集评估、混淆矩阵、保存模型、对新图预测。这种分层方式最大的好处是任何一个环节想替换都很容易。比如想把特征提取换成深度网络倒数第二层的特征向量只需要改第三层想把SVM换成别的分类器只需要改第四层的训练函数。我最初也试过把所有代码堆在一个脚本里结果改一个参数就要反复跑整个流程浪费了很多时间。5.2 关键函数速查Matlab里这个项目用到的核心函数集中在两个工具箱Image Processing Toolbox和Statistics and Machine Learning Toolbox。常用函数整理成一张表方便对照。函数作用所属工具箱imageDatastore批量读取图片按文件夹自动打标签图像处理imtophat顶帽变换提取毛发等细亮结构图像处理graythresh/imbinarizeOtsu阈值分割图像处理imfill/bwareaopen/imclose形态学清理图像处理regionprops区域面积、周长、边界框等几何属性图像处理graycomatrix/graycoprops灰度共生矩阵及纹理统计量图像处理fitcsvm训练SVM分类器统计与机器学习predict用训练好的模型预测新样本统计与机器学习cvpartition分层划分训练/测试集、KFold折统计与机器学习perfcurve计算ROC曲线和AUC统计与机器学习confusionmat输出混淆矩阵统计与机器学习5.3 主脚本骨架与单张图片预测下面是一个可直接运行的主脚本骨架。特征提取第一次跑会比较慢所以我加了一个缓存机制把特征矩阵保存成features.mat第二次直接加载。%% 1. 读取数据集 imds imageDatastore(data/, ... IncludeSubfolders, true, LabelSource, foldernames); %% 2. 批量提取特征带缓存 featureFile features.mat; if exist(featureFile, file) load(featureFile); % 包含 X 和 labels else X []; n numel(imds.Files); for i 1:n imgPath imds.Files{i}; img imread(imgPath); [mask, clean] preprocessLesion(imgPath); f extractFeatures(img, mask, clean); X [X; f]; if mod(i, 50) 0 fprintf(已处理 %d/%d\n, i, n); end end labels imds.Labels; save(featureFile, X, labels); fprintf(特征提取完成并已缓存\n); end %% 3. 训练测试划分 rng(42); cv cvpartition(labels, HoldOut, 0.2); Xtr X(training(cv), :); ytr labels(training(cv)); Xte X(test(cv), :); yte labels(test(cv)); %% 4. 训练SVM假设bestC和bestScale已经调好 mdl fitcsvm(Xtr, ytr, ... KernelFunction, rbf, ... BoxConstraint, bestC, ... KernelScale, bestScale, ... Standardize, true); %% 5. 测试评估 pred predict(mdl, Xte); C confusionmat(yte, pred); disp(C);单张新图的预测封装成一个函数方便以后接到界面或者批量脚本里function [label, score, prob] classifyNewImage(imgPath, mdl) [mask, clean] preprocessLesion(imgPath); img imread(imgPath); f extractFeatures(img, mask, clean); [label, score] predict(mdl, f); prob 1 ./ (1 exp(-score)); % 粗略映射正式场景用fitSVMPosterior end注意我这里为了示意直接把决策距离用sigmoid映射成概率这是简化做法。如果确实需要概率输出官方推荐用fitSVMPosterior把SVM的分数修正成后验概率但注意它需要额外的交叉验证样本而且在训练集完全可分时会报错。正式做筛查报告的话建议使用概率输出因为临床系统需要的不只是“良性/恶性”两个词最好还有置信度。提示训练好的模型里已经保存了标准化用的均值和标准差因为开了Standardize, true预测新图时不需要再手动归一化直接用predict就行。这是很多人容易踩的坑——总觉得自己要再算一遍均值方差结果把新样本的尺度改错了。6. 实测效果、踩坑记录与复现建议6.1 一次典型实验的混淆矩阵长什么样我用一个规模约400张、恶性样本约90张的公开皮肤镜数据集做了实验特征维度28维RBF核加10折交叉验证选参。选出来BoxConstraint10、KernelScale1在20%留出测试集上的结果大概是这样的混淆矩阵。预测\真实恶性良性恶性229良性269换算下来敏感度91.7%特异度88.5%准确率89.2%。说实话这个结果在传统手工特征方法里属于正常偏好的水平离顶级期刊论文里深度学习模型的成绩还有距离但作为基线、作业或者快速原型已经相当能打了。注意具体数字依赖数据集和特征版本不要照搬。我每次调整预处理参数后准确率都会波动两三个百分点这在医学图像小样本实验里是常态。所以复现时不要迷信某个固定数字关键是看混淆矩阵的结构敏感度是否足够高恶性样本有没有被大量漏掉。6.2 我踩过的六个坑第一个坑是不做预处理直接提特征。我第一次跑实验时偷懒跳过去毛发直接算GLCM结果纹理特征里混入大量毛发方向性响应交叉验证AUC掉了近0.05后来逐一对比才发现是毛发和反光的锅。第二个坑是忘记归一化。不设Standardize, true时颜色特征0到255和圆度指数1到10在RBF核距离计算里完全不在一个量级特征向量里颜色项主导一切其他特征形同虚设。这个坑特别隐蔽因为训练集准确率依然可能很高但测试集泛化明显变差。第三个坑是类别不平衡下的假高准确率。最初我用原始数据训练测试集准确率接近85%但一看混淆矩阵恶性样本几乎全被预测成良性。改成分层抽样加AUC选参之后模型才算真正学到东西。第四个坑是特征维度与样本量不匹配。有段时间我把颜色直方图分成32个bin特征维度拉到90多400个样本喂90维特征过拟合得厉害。后来用单特征AUC筛选到20维测试结果反而更好。小样本场景“少而精”的特征远胜“多而全”的特征。第五个坑是数据泄漏。我在做特征选择时不小心用全数据集算了p值和AUC再去做交叉验证结果评估结果虚高。正确的做法是每一步特征选择都只在训练折里做或者至少保证特征选择不接触测试集。第六个坑是随机划分的方差。只做一次80/20划分结果可能因为某次划分运气好或差而变化很大。我后来改为重复5次随机划分取均值或者直接用10折交叉验证报告均值±标准差才算得到稳定的评估结果。6.3 让实验可复现的几条规矩经历过上面这些坑之后我给自己定了几条规矩现在做任何医学图像小项目都遵循固定随机种子数据划分之前先rng(固定数)这样换台电脑跑结果一致预处理参数集中放在脚本头部不要散落在代码中间方便调整和记录特征缓存文件要标注版本号数据或预处理改了就删掉缓存重新生成避免拿旧特征配新标签训练好的模型、特征列表、评估脚本一起归档写成一份简短的README哪怕三个月后回来看也能快速重新跑通。另外保存模型时不光要保存mdl最好把特征提取函数、预处理参数、测试集路径都写进一份配置结构体。临床上如果要把模型交付给别人用这些元信息比模型本身更重要。我就遇到过模型文件还在、但特征提取的版本对不上导致预测结果完全不可信的情况。6.4 可以继续扩展的方向这套流程做完之后可以向几个方向自然扩展。一是特征升级把最后的全连接层之前的高层特征常说的深度特征提取出来和手工特征拼接或直接替换再喂给SVM小样本下往往比纯深度模型更稳。二是多分类把“良性/恶性”换成“痣、黑色素瘤、基底细胞癌”等多类用fitcecoc做一对一纠错输出码框架或者对每个类别单独训练一个二分类SVM再集成。三是区域级分析不只判断整张病灶图而是把病灶切成patch逐个patch分类最后聚合出整张图的判断这样能对应到具体异常区域给医生提供更直观的可视化依据。最后说点我个人的体会。整套系统里SVM本身反而不是难点难的是把“医生眼里的特征”翻译成“代码里的数字”。这个项目做完以后我最大的收获不是那个准确率数字而是建立了一套从临床规则出发做特征工程、再回到评估指标验证的思路。如果你手头数据很少不要一上来就搞50维特征先用10到15个最核心的ABCD特征把流程跑通再加纹理特征观察交叉验证曲线的变化这样每一步的增益和损耗都清清楚楚。做医学图像项目慢就是快。
阅读完成 · 觉得有帮助?