跳转到主要内容
文档分类的目的是将文档归入不同的预定义类别。在处理包含多种文档类型的文档流时,这项功能非常有用,因为您需要识别每份文档的类型。例如,您可能希望将合同、发票和收据分别整理到不同的文件夹中,或者根据其类型对它们重命名。借助预训练系统,这一过程可以自动完成。 文档分类的一个主要特点是,您事先就知道需要区分哪些文档类型。ABBYY FineReader Engine 可以根据文档内容或图像特征对文档进行分类,也可以同时考虑识别出的文本特征和图像特征。 下面详细介绍这一过程。它包括两个主要步骤:
  1. 创建分类数据库
为每个类别选择几份典型文档或页面。它们将用于创建分类数据库。
  1. 对文档进行分类
上一步创建的数据库可用于文档分类。输入文档会被送入预训练的分类系统,该系统使用分类数据库来确定文档所属的类别。 您也可能需要根据文档的某些属性 (如作者或条码值) 对文档进行分类。本文不涉及这种分类。如果您想根据文档属性进行分类,则应实现自己的算法,并可使用文本提取字段级识别条码识别场景提取数据。 下文所述过程也可参考适用于 Windows 的 Classification 演示工具,以及适用于 Linux 和 macOS 的 Classification 代码示例。

场景实现

本主题中提供的代码示例仅适用于 Windows。
以下将详细介绍使用 ABBYY FineReader Engine 对文档进行分类的推荐方法。
要开始使用 ABBYY FineReader Engine,您需要创建 Engine 对象。Engine 对象是 ABBYY FineReader Engine 对象层次结构中的顶层对象,提供各种全局设置、一些处理方法,以及用于创建其他对象的方法。要创建 Engine 对象,您可以使用 InitializeEngine 函数。另请参阅 加载 Engine 对象的其他方式 (Win) 。

C#

public class EngineLoader : IDisposable
{
    public EngineLoader()
    {
        // 使用 FREngine.dll 的完整路径、您的 Customer Project ID 进行初始化,
        // 以及(如适用)Online License 令牌文件的路径和 Online License 密码
        string enginePath = "";
        string customerProjectId = "";
        string licensePath = "";
        string licensePassword = "";
        // 加载 FREngine.dll 库
        dllHandle = LoadLibraryEx(enginePath, IntPtr.Zero, LOAD_WITH_ALTERED_SEARCH_PATH);
           
        try
        {
            if (dllHandle == IntPtr.Zero)
            {
                throw new Exception("Can't load " + enginePath);
            }
            IntPtr initializeEnginePtr = GetProcAddress(dllHandle, "InitializeEngine");
            if (initializeEnginePtr == IntPtr.Zero)
            {
                throw new Exception("Can't find InitializeEngine function");
            }
            IntPtr deinitializeEnginePtr = GetProcAddress(dllHandle, "DeinitializeEngine");
            if (deinitializeEnginePtr == IntPtr.Zero)
            {
                throw new Exception("Can't find DeinitializeEngine function");
            }
            IntPtr dllCanUnloadNowPtr = GetProcAddress(dllHandle, "DllCanUnloadNow");
            if (dllCanUnloadNowPtr == IntPtr.Zero)
            {
                throw new Exception("Can't find DllCanUnloadNow function");
            }
            // 将指针转换为委托
            initializeEngine = (InitializeEngine)Marshal.GetDelegateForFunctionPointer(
                initializeEnginePtr, typeof(InitializeEngine));
            deinitializeEngine = (DeinitializeEngine)Marshal.GetDelegateForFunctionPointer(
                deinitializeEnginePtr, typeof(DeinitializeEngine));
            dllCanUnloadNow = (DllCanUnloadNow)Marshal.GetDelegateForFunctionPointer(
                dllCanUnloadNowPtr, typeof(DllCanUnloadNow));
            // 调用 InitializeEngine 函数 
            // 传入 Online License 文件路径和 Online License 密码
            int hresult = initializeEngine(customerProjectId, licensePath, licensePassword, 
                "", "", false, ref engine);
            Marshal.ThrowExceptionForHR(hresult);
        }
        catch (Exception)
        {
            // 释放 FREngine.dll 库
            engine = null;
            // 在调用 FreeLibrary 之前删除所有对象
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            FreeLibrary(dllHandle);
            dllHandle = IntPtr.Zero;
            initializeEngine = null;
            deinitializeEngine = null;
            dllCanUnloadNow = null;
            throw;
        }
    }
    // Kernel32.dll 函数
    [DllImport("kernel32.dll")]
    private static extern IntPtr LoadLibraryEx(string dllToLoad, IntPtr reserved, uint flags);
    private const uint LOAD_WITH_ALTERED_SEARCH_PATH = 0x00000008;
    [DllImport("kernel32.dll")]
    private static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);
    [DllImport("kernel32.dll")]
    private static extern bool FreeLibrary(IntPtr hModule);
    // FREngine.dll 函数
    [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)]
    private delegate int InitializeEngine(string customerProjectId, string licensePath, 
        string licensePassword, string tempFolder, string dataFolder, bool isSharedCPUCoresMode, 
        ref FREngine.IEngine engine);
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    private delegate int DeinitializeEngine();
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    private delegate int DllCanUnloadNow();
    // 私有变量
    private FREngine.IEngine engine = null;
    // FREngine.dll 句柄
    private IntPtr dllHandle = IntPtr.Zero;
    private InitializeEngine initializeEngine = null;
    private DeinitializeEngine deinitializeEngine = null;
    private DllCanUnloadNow dllCanUnloadNow = null;
}
创建 ClassificationEngine 对象,它可作为其他 Classification API 对象的工厂。请使用 Engine 对象的 CreateClassificationEngine 方法。

C#

FREngine.IEngine engine;
FREngine.IClassificationEngine classEngine = engine.CreateClassificationEngine();
训练和分类方法使用一种从文档或页面创建的特殊对象:ClassificationObject,其中包含与分类相关的所有信息。要为分类场景准备文档,请按以下步骤操作:
  1. 加载待处理图像。可通过多种方式完成,例如,借助 Engine 对象的 CreateFRDocument 方法创建 FRDocument 对象,然后使用 AddImageFile 方法从文件向创建好的 FRDocument 对象中添加图像。
  2. 如果要训练或使用会考虑文本特征的分类器类型 (CT_CombinedCT_Text) ,请先通过任意方便的方法识别文档。这里我们将使用 FRDocument 对象的 AnalyzeRecognize 方法。分类不需要进行文档合成。
虽然分类本身不支持并行处理,但在 Windows 和 Linux 中,对文档进行前期识别准备时,你可能需要使用并行处理。如果待分类的文档数量较多,建议使用 Batch Processor 或 Parallel Processing with ABBYY FineReader Engine 中介绍的其他并行处理方法。
  1. 使用 ClassificationEngine 对象的 CreateObjectFromDocument 方法,创建包含文档第一页信息的 ClassificationObject。如果需要使用文档中的其他页面,请调用 CreateObjectFromPage 方法。
  2. 默认情况下,ClassificationObject 的 Description 属性为空。如果需要相关描述,请设置此属性。
有时会出现这样的情况:识别后的文档或页面实际上不包含任何已识别文本 (例如,误用了空白页) 。在这种情况下,ClassificationObject 不能用于需要文本特征的分类器。你可以使用其 SuitableClassifiers 属性再次检查。

C#

// 创建 FRDocument 对象
FREngine.IFRDocument frDocument = engine.CreateFRDocument();
// 添加图像
frDocument.AddImageFile( "C:\\MyImage.tif", null, null );
// 可选:分析并识别文档
frDocument.Analyze( null, null, null );
frDocument.Recognize( null, null );
// 创建分类对象
FREngine.IClassificationObject clObject = classEngine.CreateObjectFromDocument( frDocument );
// 将该对象所属类别写入其描述中
clObject.Description = "CategoryA_Object1";
要训练一个能够区分多种文档类型的分类器,你需要一个按类别组织的数据集,其中包含每种类型的样本。使用 TrainingData 对象填充并管理此数据集:
  1. 使用 ClassificationEngine 对象的 CreateTrainingData 方法创建一个空对象。
  2. 通过 Categories 属性访问类别集合。
  3. 多次调用 Categories 对象的 AddNew 方法,为要分类的每种文档类型添加一个类别。该方法要求输入一个表示类别标签的 string。该标签会由分类方法返回,因此在类别集合中必须唯一。
  4. 对于每个新添加的 Category 对象,使用 Objects 属性访问分类对象集合。借助 IClassificationObjects::Add 方法,添加与该类别对应的分类对象。
    任何类别都不能为空。显然,训练至少需要两个类别。
  5. 配置好训练数据集后,你可能希望将其保存到磁盘上的文件中,以便后续使用:例如,如果训练出的模型精度不理想,而你希望添加或更正一些数据来提高质量。TrainingData 对象提供了 SaveToFile 方法。

C#

FREngine.ITrainingData trainingData = classEngine.CreateTrainingData();
FREngine.ICategories categories = trainingData.Categories;
// 添加第一个类别
FREngine.ICategory category = categories.AddNew( "CategoryA" );
// 添加在第 3 步中准备好的分类对象
category.Objects.Add( clObject ); // 对此类别中的所有对象重复此操作
...
// 对所有类别重复此操作
...
// 添加完所有类别后,保存训练数据集
trainingData.SaveToFile( "C:\\trainingData.dat" );
模型训练功能由 Trainer 对象提供。使用 ClassificationEngine 对象的 CreateTrainer 方法可创建该对象。它包含分类器类型和训练过程的所有设置,分布在两个子对象 TrainingParamsValidationParams 中。请根据需要确定相应设置并修改对应的属性:
  • 分类器的类型 (ITrainingParams::ClassifierType) 。此设置决定了在分配类别时会考虑文档的哪些特征:图像特征、已识别的文本内容,或两者兼顾。要选择使用文本内容的类型,您需要确保训练数据集中的所有分类对象均由先前已识别的文档创建。
  • 训练模式 (ITrainingParams::TrainingMode) 。此设置决定训练过程应优先追求高精确率 (选中的元素中有多少是正确的) 、高召回率 (有多少正确元素被选中) ,还是在两者之间取得平衡。
  • 是否应使用 k 折交叉验证 (IValidationParams::ShouldPerformValidation) 。当训练样本量较小时,我们建议使用交叉验证,因为这样可以基于同一样本的不同分区训练多个模型,并从中选出最佳模型。如果您拥有大量已分类数据,则最好关闭验证,在整个训练样本上训练模型,然后使用分类方法 (步骤 6) 在另一组样本上测试该模型,并自行计算性能指标。
  • k 折交叉验证的参数包括:训练样本被划分的份数 (IValidationParams::FoldsCount) 以及迭代次数 (IValidationParams::RepeatCount) 。请注意,对于 文本分类器,每次迭代中的训练集所需对象数不能少于 4;对于 组合分类器,不能少于 8。请确保训练样本中包含足够数量的对象。
现在可以开始训练模型了。将第 4 步中配置的 TrainingData 对象传递给 Trainer 对象的 TrainModel 方法。该方法将返回一个 TrainingResults 集合,在当前可用功能下,该集合仅包含一个 TrainingResult。如果选择执行交叉验证,请查看其 ValidationResult 子对象中的性能评分。
模型训练和分类将在 Linux 和 Windows 中以串行模式执行,而不受 IMultiProcessingParams::MultiProcessingMode 值的影响。
ITrainingResult::Model 属性用于访问已训练的分类模型。您可以通过 SaveToFile 方法将其保存到文件,也可以直接用于对文档进行分类 (继续执行步骤 6) 。

C#

// 创建训练器对象并设置参数
FREngine.ITrainer trainer = classEngine.CreateTrainer();
trainer.TrainingParams.ClassifierType = (int)FREngine.ClassifierTypeEnum.CT_Image; // 分类器将仅使用图像特征
// 其余设置保持默认,直接训练模型
FREngine.ITrainingResults results = trainer.TrainModel ( trainingData );
// 检查模型的 F1 分数
double F1 = results[0].ValidationResult.FMeasure;
// 获取分类模型
FREngine.IModel model = results[0].Model;
// 保存模型以备后用
model.SaveToFile( "C:\\model.dat" );
要使用训练后的模型进行分类,请执行以下操作:
  1. 如果模型当前尚未加载,请调用 ClassificationEngine 对象的 CreateModelFromFile 方法,从磁盘上的文件加载模型。
  2. 按照步骤 3 中所述,根据需要分类的文档准备分类对象。
  3. 对每个分类对象,调用 Model 对象的 Classify 方法,并将 ClassificationObject 作为输入参数。该方法会返回一个 ClassificationResult 对象集合,其中每个对象都包含类别标签以及该类别的概率。结果会按概率从高到低排序。获取结果并检查该概率值是否在您可接受的范围内。
    如果分类器无法分配类别,则返回 null,而不是结果集合。
无论 IMultiProcessingParams::MultiProcessingMode 的值如何,模型训练和分类在 Linux 和 Windows 中都将以顺序模式执行。

C#

// 打开训练后的模型
FREngine.IModel model = classEngine.CreateModelFromFile( "C:\\model.dat" );
// 对对象进行分类
FREngine.IClassificationResults classResults = model.Classify( clObject );
// 获取最佳结果及其概率
string label = classResults[0].CategoryLabel;
double probability = classResults[0].Probability;
完成 ABBYY FineReader Engine 的使用后,您需要卸载 Engine 对象。为此,请使用导出的 DeinitializeEngine 函数。

C#

public class EngineLoader : IDisposable
{
    // 卸载 FineReader Engine
    public void Dispose()
    {
        if (engine == null)
        {
            // Engine 未加载
            return;
        }
        engine = null;
        // 在调用 FreeLibrary 之前删除所有对象
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        int hresult = deinitializeEngine();
 
        hresult = dllCanUnloadNow();
        if (hresult == 0)
        {
            FreeLibrary(dllHandle);
        }
        dllHandle = IntPtr.Zero;
        initializeEngine = null;
        deinitializeEngine = null;
        dllCanUnloadNow = null;
        // 清理完成后抛出异常
        Marshal.ThrowExceptionForHR(hresult);
    }
    // Kernel32.dll 函数
    [DllImport("kernel32.dll")]
    private static extern IntPtr LoadLibraryEx(string dllToLoad, IntPtr reserved, uint flags);
    private const uint LOAD_WITH_ALTERED_SEARCH_PATH = 0x00000008;
    [DllImport("kernel32.dll")]
    private static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);
    [DllImport("kernel32.dll")]
    private static extern bool FreeLibrary(IntPtr hModule);
    // FREngine.dll 函数
    [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)]
    private delegate int InitializeEngine( string customerProjectId, string LicensePath, string LicensePassword, , , , ref FREngine.IEngine engine);
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    private delegate int DeinitializeEngine();
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    private delegate int DllCanUnloadNow();
    // 私有变量
    private FREngine.IEngine engine = null;
    // FREngine.dll 句柄
    private IntPtr dllHandle = IntPtr.Zero;
    private InitializeEngine initializeEngine = null;
    private DeinitializeEngine deinitializeEngine = null;
    private DllCanUnloadNow dllCanUnloadNow = null;
}

必需资源

您可以使用 FREngineDistribution.csv 文件,自动生成应用程序正常运行所需的文件列表。对于此场景下的处理,请在第 5 列 (RequiredByModule) 中选择以下值: Core Core.Resources Opening Opening, Processing Processing Processing.Classification Processing.Classification.NaturalLanguages Processing.OCR Processing.OCR, Processing.ICR Processing.OCR.NaturalLanguages Processing.OCR.NaturalLanguages, Processing.ICR.NaturalLanguages 如果您修改了标准场景,请相应调整所需模块。您还需要指定界面语言、识别语言,以及应用程序使用的其他功能 (例如,如果需要打开 PDF 文件,则指定 Opening.PDF;如果需要识别 CJK languages 中的文本,则指定 Processing.OCR.CJK) 。更多详细信息,请参阅 Working with the FREngineDistribution.csv File

进一步优化

您可以在以下文章中了解有关配置各个处理阶段的更多信息:

另请参阅

基本使用场景的实现