Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 6개 있습니다.)
.NET Framework: 2116. C# - OpenAI API 사용 - 지원 모델 목록
; https://www.sysnet.pe.kr/2/0/13344

닷넷: 2165. C# - Azure OpenAI API를 이용해 ChatGPT처럼 동작하는 콘솔 응용 프로그램 제작
; https://www.sysnet.pe.kr/2/0/13451

닷넷: 2166. C# - Azure OpenAI API를 이용해 사용자가 제공하는 정보를 대상으로 검색하는 방법
; https://www.sysnet.pe.kr/2/0/13452

닷넷: 2167. C# - Qdrant Vector DB를 이용한 Embedding 벡터 값 보관/조회 (Azure OpenAI)
; https://www.sysnet.pe.kr/2/0/13454

닷넷: 2168. C# - Azure.AI.OpenAI 패키지로 OpenAI 사용
; https://www.sysnet.pe.kr/2/0/13455

닷넷: 2169. C# - OpenAI를 사용해 PDF 데이터를 대상으로 OpenAI 챗봇 작성
; https://www.sysnet.pe.kr/2/0/13456




C# - OpenAI를 사용해 PDF 데이터를 대상으로 OpenAI 챗봇 작성

이번 글도 ^^ .NET Conf 2023에 있었던 동영상을 그대로 베끼겠습니다.

Build an Azure OpenAI powered .NET 8 Chat Bot on your data from scratch | .NET Conf 2023
; https://youtu.be/fYJuokUnucE




지난 글에서,

C# - Azure OpenAI API를 이용해 사용자가 제공하는 정보를 대상으로 검색하는 방법
; https://www.sysnet.pe.kr/2/0/13452

C# - Qdrant Vector DB를 이용한 Embedding 벡터 값 보관/조회 (Azure OpenAI)
; https://www.sysnet.pe.kr/2/0/13454

대략 다음과 같은 처리 순서를 설명했습니다.

  1. 사용자가 제공하는 문서를 embedding 시켜 벡터로 보관 (대개의 경우 DB에 보관)
  2. 사용자 입력한 쿼리를 embedding 시키고, 1번 과정에서 저장한 것과 비교해 적절한 문서를 선택(혹은 DB로부터 조회)
  3. 조회한 문서와 함께 사용자가 입력한 쿼리를 ChatGPT에 전달

저 의미에서 보면, PDF 역시 사용자가 제공하는 문서에 불과하므로, 1번 과정을 거쳐 embedding 시켜 벡터로 보관해 두는 작업을 거쳐야 합니다. 자, 그럼 당연히 PDF 문서를 읽는 라이브러리가 필요하겠죠. ^^

Install-Package itext7

그다음 적절한 PDF 예제 문서가 있어야 하는데, 테스트를 위해 너무 큰 PDF 문서를 지정하면 OpenAI/Azure 사용료만 부과되므로 적절하게 10개 페이지 이하 분량의 PDF를 하나 선택해 주고,

Shared Files PRO - A WordPress plugin by Tammersoft
  - Sample PDF
; https://www.sharedfilespro.com/shared-files/38/sample.pdf

다음의 코드를 이용해 페이지 하나 당 Embedding 시킨 벡터 값들을 구해 Qdrant에 저장해 둡니다.

string qdrantHost = "localhost";
QdrantClient qdrantClient = new QdrantClient(qdrantHost, 6334, false);
string collectionName = "pdf_docs";

await EmbedPdfFilesAsync(qdrantClient, collectionName, openAIClient, embeddingDeployment,
    "sample.pdf");

private static string[] ReadPdfFile(string filePath)
{
    using PdfDocument pdfDoc = new PdfDocument(new PdfReader(filePath));
    List<string> pages = new List<string>();

    for (int page = 1; page <= pdfDoc.GetNumberOfPages(); page++)
    {
        PdfPage pdfPage = pdfDoc.GetPage(page);
        ITextExtractionStrategy strategy = new SimpleTextExtractionStrategy();
        pages.Add(PdfTextExtractor.GetTextFromPage(pdfPage, strategy));

        Console.WriteLine($"Page {page}: # of chars = {pages[page - 1].Length}");
    }

    return pages.ToArray();
}

private static async Task EmbedPdfFilesAsync(
    QdrantClient qdrantClient, string collectionName, OpenAIClient openAIClient, string embeddingDeployment,
    string pdfFile)
{
    var collections = await qdrantClient.ListCollectionsAsync();
    if (collections.Contains(collectionName))
    {
        // await qdrantClient.DeleteCollectionAsync(collectionName);
        return;
    }

    string[] pdfPages = ReadPdfFile(pdfFile);

    var emddedPages =
        pdfPages.Select(page => new EmbeddedPage(page, [])).ToArray();

    var tokenizer = await Tokenizer.CreateAsync(TokenizerModel.ada2);

    foreach (var (page, index) in emddedPages.WithIndex())
    {
        var fullText = page.Text;
        if (string.IsNullOrWhiteSpace(fullText))
        {
            continue;
        }

        int tokenCount = tokenizer.GetTokenCount(fullText);
        Console.WriteLine($"Page {index + 1} - # of tokens = {tokenCount}");

        var chunks = tokenizer.ChunkByTokenCountWithOverlap(fullText, 3000, 50).Chunk(16).ToArray();

        foreach (var chunk in chunks)
        {
            var embeddingResponse = await openAIClient.GetEmbeddingsAsync(
                new EmbeddingsOptions(embeddingDeployment, chunk));

            page.Chunks.AddRange(
                embeddingResponse.Value.Data.Select(d =>
                new TextWithEmbedding(chunk[d.Index], d.Embedding.ToArray())));
        }
    }

    await qdrantClient.CreateCollectionAsync(collectionName,
        new VectorParams { Size = 1536, Distance = Distance.Cosine });

    var vectors = emddedPages
        .Where(d => d.Chunks.Count > 0)
        .SelectMany(d =>
        d.Chunks.Select(c => new
        {
            Embedding = c.Embedding,
            Text = d.Text,
        }))
        .ToList();

    var points = vectors.Select(vector =>
    {
        var point = new PointStruct
        {
            Id = new PointId { Uuid = Guid.NewGuid().ToString() },
            Vectors = vector.Embedding,
            Payload =
            {
                ["text"] = vector.Text
            }
        };

        return point;
    }).ToList();

    await qdrantClient.UpsertAsync(collectionName, points);
}

지난번 코드와 비교하면 PDF 데이터로 바뀌었다는 점을 제외하고는 거의 그대로 재사용이 되었습니다.

테스트로 사용한 PDF는 5개의 페이지를 포함하고 있는데요, 그래서 위의 코드를 실행하면 다음과 같은 결과를 볼 수 있습니다.

Page 1: # of chars = 3062
Page 2: # of chars = 2476
Page 3: # of chars = 2696
Page 4: # of chars = 1647
Page 5: # of chars = 899

Page 1: # of tokens = 617
Page 2: # of tokens = 514
Page 3: # of tokens = 576
Page 4: # of tokens = 342
Page 5: # of tokens = 453

달리 말하면, 웬만한 PDF 페이지는 Token 수가 1,000개를 넘지 않으므로 페이지 단위 정도라면 OpenAI 측의 대화 문맥으로 사용하는데 부담이 없는 수준입니다. (물론, 좀 더 검색 수준을 높이고 싶다면 페이지 단위보다는, 허용이 되는 수준의 장(Chapter) 또는 절(Section) 단위로 문서 구분을 하는 것도 좋을 것입니다.)

어쨌든, 위와 같은 상황에서 검색을 해보면,

string question = "What is the features of Pdf995 Suite solution?";

string[] results = await SearchWithQdrantAsync(qdrantClient, collectionName,
    openAIClient, embeddingDeployment,
    question, 5); // 5개 페이지인데, limit을 5로 설정했으니 모든 페이지를 반환

results.All((text) =&gt;
{
    Console.WriteLine(text);
    Console.WriteLine("-----------------------------------");
    return true;
});

public static async Task<string[]> SearchWithQdrantAsync(
    QdrantClient qdrantClient, string collectionName,
    OpenAIClient openAIClient, string embeddingDeployment,
    string query, int resultLimit = 1)
{
    var embeddingResponse = await openAIClient.GetEmbeddingsAsync(
                    new EmbeddingsOptions(embeddingDeployment, new[] { query }));

    var embeddingVector = embeddingResponse.Value.Data[0].Embedding.ToArray();

    var results = await qdrantClient.SearchAsync(collectionName, embeddingVector,
        limit: (ulong)resultLimit);

    foreach (var result in results)
    {
        Console.WriteLine($"Score: {result.Score}");
    }

    return results.Select(r => r.Payload["text"].StringValue).ToArray();
}

검색 결과에 따른 개별 문서(위의 예에서는 페이지)별 Cosine 유사도는 다음과 같이 나옵니다.

Score: 0.9092298
Score: 0.7339479
Score: 0.70261437
Score: 0.70222175
Score: 0.69459313

실제 서비스라면 전체 문서를 모두 검색 결과로 받지는 않을 것이므로, 대략 0.8 이상의 유사도가 나오는 문서를 검색하도록 Qdrant 검색 조건에 주는 것이 좋겠습니다.

var results = await qdrantClient.SearchAsync(collectionName, embeddingVector,
    scoreThreshold: 0.8f, limit: (ulong)resultLimit);




자, 그럼 이렇게 DB를 구축했으니 이후부터는 사용자로부터 질문을 받고, ChatGPT처럼 답하는 코드를 작성할 수 있습니다.

string question = "...[사용자가 입력한 질문]...";

string[] results = await SearchWithQdrantAsync(qdrantClient, collectionName,
    openAIClient, embeddingDeployment,
    question, 5);

var chatCompletionsOptions = new ChatCompletionsOptions()
{
    DeploymentName = deploymentModel,
    MaxTokens = 1000,
    Temperature = 0, // Knowledge Base 조회인 경우, 정확함을 목표로 하게 되므로 0으로 지정
    Messages =
    {
        new ChatMessage(ChatRole.System, "You are a helpful AI assistant"),
        new ChatMessage(ChatRole.User, "The following information is from the PDF text: " + string.Join('\n', results)),
        new ChatMessage(ChatRole.User, question),
    }
};

Response<ChatCompletions> response = openAIClient.GetChatCompletions(chatCompletionsOptions);

Console.WriteLine(response.Value.Choices.First().Message.Content);

보는 바와 같이, DB로부터 조회한 문서 데이터와 함께 사용자의 질문을 chat model에게 전달해 응답을 받아 처리하고 있습니다. 실제로 실행해 보면 이런 결과를 얻게 됩니다.

[질문]: What is the features of Pdf995 Suite solution?

[답변]

The Pdf995 Suite of products offers the following features:

- Creation of professional-quality PDF documents
- Easy-to-use interface for creating PDF files
- Network file saving
- Fast user switching on XP
- Citrix/Terminal Server support
...[생략]...
- Specify PDF document properties
- Control PDF opening mode
- Can be configured to add functionality to Acrobat Distiller
- Free: Creates PDFs without annoying watermarks
- Free: Fully functional, not a trial and does not expire
- Over 5 million satisfied customers
- Over 1000 Enterprise Customers worldwide

All of these features are available at no cost to the user.


[질문]: What Pdf995 product is for?

[답변]

The Pdf995 suite of products, which includes Pdf995, PdfEdit995, and Signature995, is a complete solution for document publishing needs. It provides ease of use, flexibility in format, and industry-standard security, all at no cost to the user. Pdf995 makes it easy and affordable to create professional-quality documents in the popular PDF file format. PdfEdit995 offers additional functionality, such as combining documents into a single PDF, automatic link insertion, and PDF conversion to HTML or DOC. Signature995 offers state-of-the-art security and encryption to protect documents and add digital signatures.


이 정도면 대충 감이 오시죠? ^^

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




여러분만의 Knowledge Base를 embedding 시킨 DB가 있다면, 물론 예전에도 Elasticsearch와 같은 검색 엔진을 사용할 수 있었지만 OpenAI의 Chat Completion 기능과 함께 연동하면 좀 더 자연스러운 수준의 검색 결과를 받아올 수 있습니다.

뭐랄까... 달리 생각하면 해당 DB 하나가 자신의 또 다른 두뇌 저장소라고 봐도 좋을 개념이 된 것입니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 11/25/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2023-12-17 06시03분
[Langchain] PDF를 학습한 나만의 챗봇 streamlit에 배포 하기
; https://unfinishedgod.netlify.app/2023/12/16/langchain-pdf-streamlit/

SciSharp/LLamaSharp
; https://github.com/SciSharp/LLamaSharp
정성태

... 91  92  93  94  95  96  97  [98]  99  100  101  102  103  104  105  ...
NoWriterDateCnt.TitleFile(s)
11485정성태4/11/201823687.NET Framework: 738. C# - Console 프로그램이 Ctrl+C 종료 시점을 감지하는 방법파일 다운로드1
11484정성태4/11/201824764.NET Framework: 737. C# - async를 Task 타입이 아닌 사용자 정의 타입에 적용하는 방법파일 다운로드1
11483정성태4/10/201828066개발 환경 구성: 358. "Let's Encrypt"에서 제공하는 무료 SSL 인증서를 IIS에 적용하는 방법 (2) [1]
11482정성태4/10/201820497VC++: 126. CUDA Core 수를 알아내는 방법
11481정성태4/10/201832195개발 환경 구성: 357. CUDA의 인덱싱 관련 용어 - blockIdx, threadIdx, blockDim, gridDim
11480정성태4/9/201822189.NET Framework: 736. C# - API를 사용해 Azure에 접근하는 방법 [2]파일 다운로드1
11479정성태4/9/201817815.NET Framework: 735. Azure - PowerShell로 Access control(IAM)에 새로운 계정 만드는 방법
11478정성태11/8/201920083디버깅 기술: 115. windbg - 덤프 파일로부터 PID와 환경변수 등의 정보를 구하는 방법 [1]
11477정성태4/8/201817508오류 유형: 460. windbg - sos 명령어 수행 시 c0000006 오류 발생
11476정성태4/8/201819084디버깅 기술: 114. windbg - !threads 출력 결과로부터 닷넷 관리 스레드(System.Threading.Thread) 객체를 구하는 방법
11475정성태3/28/201821391디버깅 기술: 113. windbg - Thread.Suspend 호출 시 응용 프로그램 hang 현상에 대한 덤프 분석
11474정성태3/27/201819523오류 유형: 459. xperf: error: TEST.Event: Invalid flags. (0x3ec).
11473정성태3/22/201824622.NET Framework: 734. C# - Thread.Suspend 호출 시 응용 프로그램 hang 현상파일 다운로드2
11472정성태3/22/201818614개발 환경 구성: 356. GTX 1070, GTX 960, GT 640M의 cudaGetDeviceProperties 출력 결과
11471정성태3/20/201821991VC++: 125. CUDA로 작성한 RGB2RGBA 성능 [1]파일 다운로드1
11470정성태3/20/201824179오류 유형: 458. Visual Studio - CUDA 프로젝트 빌드 시 오류 C1189, expression must have a constant value
11469정성태3/19/201817202오류 유형: 457. error MSB3103: Invalid Resx file. Could not load file or assembly 'System.Windows.Forms, ...' or one of its dependencies.
11468정성태3/19/201816700오류 유형: 456. 닷넷 응용 프로그램 실행 시 0x80131401 예외 발생
11467정성태3/19/201816104오류 유형: 455. Visual Studio Installer - 업데이트 실패
11466정성태3/18/201817242개발 환경 구성: 355. 한 대의 PC에서 2개 이상의 DirectX 게임을 실행하는 방법
11463정성태3/15/201819608.NET Framework: 733. 스레드 간의 read/write 시에도 lock이 필요 없는 경우파일 다운로드1
11462정성태3/14/201822527개발 환경 구성: 354. HTTPS 호출에 대한 TLS 설정 확인하는 방법 [1]
11461정성태3/13/201825090오류 유형: 454. 윈도우 업데이트 설치 오류 - 0x800705b4 [1]
11460정성태3/13/201817579디버깅 기술: 112. windbg - 닷넷 메모리 덤프에서 전역 객체의 내용을 조사하는 방법
11459정성태3/13/201818392오류 유형: 453. Debug Diagnostic Tool에서 mscordacwks.dll을 찾지 못하는 문제
11458정성태2/21/201819380오류 유형: 452. This share requires the obsolete SMB1 protocol, which is unsafe and could expose your system to attack. [1]
... 91  92  93  94  95  96  97  [98]  99  100  101  102  103  104  105  ...