diff --git a/.github/workflows/jekyll.yml b/.github/workflows/jekyll.yml index 5b30ece..5800afa 100644 --- a/.github/workflows/jekyll.yml +++ b/.github/workflows/jekyll.yml @@ -72,20 +72,21 @@ jobs: JEKYLL_ENV: production - name: Validate HTML, internal links, and images # Catches broken internal links/anchors and missing images before deploy. - # External links are skipped (slow, flaky); hash-only hrefs are allowed. + # External links are skipped (slow, flaky) but must be https; hash-only + # hrefs are allowed. # --swap-urls strips the Pages base_path so absolute /baseurl/... links # resolve against _site/ regardless of what base_path the build used. run: | bundle exec htmlproofer ./_site \ --disable-external \ --allow-hash-href \ - --no-enforce-https \ --swap-urls "^${{ steps.pages.outputs.base_path }}:" - name: Validate discoverability (sitemap, feed, metadata) # Guards what htmlproofer does not look at: that the sitemap and feed - # parse, that every page carries a description and a canonical, and that - # no two pages share a description or title. Each check maps to a defect - # this site actually shipped. + # parse, that every page carries a description and a canonical, that no + # two pages share a description or title, and that each plugin hook left + # its mark on the posts (script/check-plugin-output.rb). Each check maps + # to a defect this site actually shipped. run: script/validate-site.sh ./_site - name: Upload artifact # Nothing to publish from a PR run; the gates above are the whole point. diff --git a/README.ko.md b/README.ko.md index fc69549..8a96de4 100644 --- a/README.ko.md +++ b/README.ko.md @@ -49,7 +49,7 @@ bundle exec jekyll serve `_posts/`, `_sass/`, `_includes/` 아래 파일을 고치고 저장한 뒤 새로고침하면 대부분 바로 반영됩니다. 단, `_config.yml`을 바꿨을 때는 서버를 다시 시작해야 합니다. -**4. 최종 결과물이 필요하면 프로덕션 빌드를 합니다** (CI가 하는 것과 동일). +**4. 최종 결과물이 필요하면 빌드합니다** (CI 옵션은 `.github/workflows/jekyll.yml`, 로컬 검사는 "푸시 전에 확인하세요" 참고). ```bash bundle exec jekyll build # 결과물은 _site/ 에 생성 @@ -81,7 +81,7 @@ _plugins/ reading_time.rb (한·영 읽기 시간 계산) search_index.rb (search.json용 plain_text 필터) legacy_urls.rb (슬러그 이전의 옛 글 URL 리디렉션) translations.rb (번역본을 글 목록에서 빼기) - posted_date.rb (파일명 날짜로 last_modified_at 채우기) + posted_date.rb (파일명 날짜가 더 늦으면 last_modified_at 채우기) css/ main.scss(Sass 진입점) · search.css(검색 페이지 전용) js/ main.js(테마 토글·코드 복사·목차·메뉴·이미지 확대 등) search.js(검색창 동작) @@ -91,9 +91,12 @@ search.json 전체 본문 검색 색인 (simple-jekyll-search가 사용) test/ _plugins/ 로직의 minitest 단위 테스트 script/ lint-posts.rb (빌드 전 글 원본 규칙 검사) validate-site.sh (빌드 후 검색 노출 검사) + check-plugin-output.rb (플러그인 훅이 글에 남긴 결과 검사, + validate-site.sh가 실행) _data/ post_conventions.yml (카테고리·표지·통제 태그·폐기 태그의 유일한 정본. scholar-lens도 읽음) .github/workflows/ CI: 테스트 → 린트 → 빌드 → html-proofer → validate-site, main 푸시 시 배포 + workflow-lint.yml: actionlint와 shellcheck ``` **최상위 페이지:** `index.html`(홈), 네 개의 섹션 페이지인 `paper-reviews.md`· @@ -116,14 +119,15 @@ subtitle: "<한 줄 소개>" # 선택 — 헤더의 제목 아래에 date: YYYY-MM-DD HH:MM:SS paper_author: "<기관>" # 논문을 낸 기관(사람 이름 아님). Insights·의견 글이면 생략 seo_title: "<짧은 제목>" # 선택 — 논문 제목이 너무 길 때 에 대신 씁니다 -description: >- # 선택 — 아래 설명 참고 +description: >- # 선택, 평문 — 아래 설명 참고 <검색 결과에 노출될 문장, 150자 안팎> categories: ["<유형>", "<주제>"] tags: ["<태그-1>", "<태그-2>"] -cover: /assets/images/<topic>.(jpg|png) +cover: /assets/images/<topic>.(jpg|png) # Insights는 insights.jpg use_math: true # 수식이 있을 때만 (MathJax를 불러옵니다) lang: ko # translation_id를 쓰면 필수(ko | en)… translation_id: <공통-슬러그> # …한국어 글과 -en 번역본을 연결합니다. + # 두 글 모두에 씁니다(린트가 짝을 확인). # 목록에는 한국어 글만 나옵니다 --- ``` @@ -143,7 +147,15 @@ RSS `<summary>`에 실리는 문장입니다. 생략하면 `_plugins/post_descri 고지로 시작하는 글 — 즉 대부분의 Insights 글 — 은 직접 쓰는 편이 낫습니다. 설명문은 **사이트 전체에서 중복되면 안 됩니다.** 두 페이지가 같은 설명문을 쓰면 CI가 -빌드를 실패시킵니다. +빌드를 실패시킵니다. 설명문은 평문으로 나가므로 `$$`, `*강조*`, 백틱, 링크, 태그를 쓰면 +린트가 막습니다. + +### 날짜, 파일명, Liquid + +파일명은 게시일(`YYYY-MM-DD-`)로 시작합니다. `date:`는 그보다 이를 수(논문 발행일)는 있어도 +늦을 수는 없습니다. Jekyll은 모든 글을 먼저 Liquid로 렌더하므로, 코드 블록의 `{{ … }}`나 +`{% … %}`(프롬프트 템플릿, GitHub Actions의 `${{ … }}`)는 빈칸이 되거나 빌드를 멈춥니다. +`{% raw %}…{% endraw %}`로 감쌉니다. 세 가지 모두 린트가 확인합니다. ### 카테고리가 URL을 결정합니다 @@ -154,7 +166,7 @@ RSS `<summary>`에 실리는 문장입니다. 생략하면 `_plugins/post_descri **탭을 유지하고** 빈 상태 문구를 띄웁니다 — 탭이 없으면 채우는 중인 섹션이 아니라 없어진 섹션으로 읽히기 때문입니다. - `categories[1]` — **주제**: `_data/post_conventions.yml`의 `topics` 중 하나이며, 표지 - 이미지도 거기서 정해집니다. 맞는 주제가 없으면 먼저 거기에 추가합니다. 목록에 없는 값은 + 이미지도 거기서 정해집니다(Insights 글은 유형의 `insights.jpg`를 씁니다). 맞는 주제가 없으면 먼저 거기에 추가합니다. 목록에 없는 값은 린트가 막습니다. Jekyll은 이 둘을 슬러그로 바꾸고(`_config.yml`의 `permalink`) 날짜와 합쳐 출력 경로를 @@ -208,14 +220,22 @@ kramdown이 내용을 그대로 보존해 `\(…\)`로 내보내므로 안전합 - `\\(…\\)`·`\\[…\\]`로 쓴 수식, 단일 `$` 옆의 TeX: 위와 같이 마크다운이 수식을 먼저 읽습니다. -- `$$…$$` 안의 `\_`: MathJax가 역슬래시와 밑줄을 그대로 보여 줍니다. `_`로 쓰고, +- `$$…$$` 안의 `\_`: MathJax가 아래첨자가 아닌 밑줄 문자를 그대로 그립니다. `_`로 쓰고, `\text{…}` 안에서만 이스케이프합니다. - 앞뒤에 빈 줄이 없는 디스플레이 `$$…$$` 줄(목록 안 포함): kramdown이 문단에 붙여 인라인 수식으로 처리합니다. - `$$` 구간 안의 한글: 거의 언제나 통화 표기 `$$` 두 개가 한 구간으로 읽힌 것입니다. - -인라인 수식에서는 맨 `|` 대신 `\lvert … \rvert`를 씁니다. 표 안에서는 맨 `|`가 셀 구분자로 -읽힙니다. +- `use_math: true` 없이 쓴 `$$` 수식: MathJax가 로드되지 않아 TeX가 글자로 보입니다. +- HTML이 아닌 `<이름>`, `<s>`·`<input>`, 또는 속성 없는 `<img>`(모델의 특수 토큰): kramdown이 그대로 + 넘기고 브라우저가 지웁니다. `코드`로 씁니다. +- 마커 뒤에 언어 말고 다른 것이 붙은 코드 펜스(```` ```python title="a" ````)나 닫히지 않은 + 펜스: kramdown이 펜스로 읽지 않습니다. +- 글마다 대소문자를 다르게 쓴 태그(`Grouped-Query-…`와 `Grouped-query-…`). +- 표 행의 `$$` 안에 있는 `|`: kramdown이 거기서 셀을 나누고, `\|`는 `|`로 바꿉니다(노름이 + 절댓값이 됩니다). `\lVert … \rVert`를 씁니다. + +인라인 수식에서는 맨 `|` 대신 `\lvert … \rvert`(또는 `\mid`)를, 노름에는 `\lVert … \rVert`를 씁니다. 린트는 표 안에서만 +잡습니다. ### 푸시 전에 확인하세요 @@ -228,8 +248,9 @@ ruby script/lint-posts.rb # 글 원본이 규 bundle exec jekyll build --strict-front-matter \ --destination /tmp/site-verify # 빌드가 깨끗한가? bundle exec htmlproofer /tmp/site-verify --disable-external \ - --allow-hash-href --no-enforce-https # 깨진 링크·이미지·앵커는 없나? + --allow-hash-href # 깨진 링크·이미지·앵커, http:// 링크는 없나? script/validate-site.sh /tmp/site-verify # 사이트맵·피드·메타데이터·헤딩 +shellcheck script/*.sh assets/images/*.sh && actionlint # workflow-lint.yml이 PR과 main에서 실행 ``` > **검사 결과가 말이 안 되면 먼저 떠 있는 `jekyll serve`를 찾으세요.** 파일을 감시하며 @@ -259,12 +280,8 @@ script/validate-site.sh /tmp/site-verify # 사이트맵·피 마크다운 원본의 표·수식·헤딩 규칙을 검사하고 3. `JEKYLL_ENV=production`으로 사이트를 빌드하고 4. `_site/`에 **html-proofer**를 돌려 내부 링크·이미지·앵커를 검사하고 -5. **`script/validate-site.sh`**로 사이트맵·피드가 byte 0에서 파싱되는지, 사이트맵 URL이 - 설정된 `url` 아래인지, 빌드 타임존이 고정됐는지, `robots.txt`가 막지 않고 사이트맵을 가리키는지, 렌더된 - 페이지가 1개 이상인지, 페이지당 `h1`이 1개인지, 헤딩 레벨을 건너뛰지 않는지, 모든 - 페이지에 설명문·canonical이 있는지, 설명문이 자기 제목보다 긴지, 설명문·제목이 - 중복되지 않는지, 목록 페이지에 번역본이 원본과 함께 나오지 않는지, 저작 원본이 - 배포되지 않았는지를 검사한 뒤 +5. **`script/validate-site.sh`**로 htmlproofer가 보지 않는 빌드 후 검사(사이트맵, 피드, + 메타데이터, 헤딩, 리디렉션, 플러그인 결과)를 돌린 뒤 — 항목은 스크립트에 있습니다 6. GitHub Pages에 배포합니다. 실패하면 대개 4·5단계입니다. Actions 로그에 어떤 링크·이미지·페이지가 문제인지 그대로 diff --git a/README.md b/README.md index f8bb4c1..44a739c 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,8 @@ bundle exec jekyll serve Edit a file under `_posts/`, `_sass/`, or `_includes/`, save, and refresh — most changes appear immediately. (Changes to `_config.yml` need a server restart.) -**4. Build for production** (what CI does) when you want the final output in -`_site/`: +**4. Build** when you want the final output in `_site/` (CI's flags are in +`.github/workflows/jekyll.yml`; "Validate before pushing" has the local gates): ```bash bundle exec jekyll build @@ -83,7 +83,7 @@ _plugins/ reading_time.rb (KO/EN-aware read time) search_index.rb (plain_text filter for search.json) legacy_urls.rb (redirects pre-slug post URLs) translations.rb (keeps a post's translation out of the lists) - posted_date.rb (last_modified_at from the file-name date) + posted_date.rb (last_modified_at from a later file-name date) css/ main.scss (Sass entry point) · search.css (search page only) js/ main.js (theme toggle, code-copy, TOC, menu, image zoom…) search.js (drives the search box) @@ -93,9 +93,12 @@ search.json Full-text search index (consumed by simple-jekyll-search) test/ minitest unit tests for the _plugins/ logic script/ lint-posts.rb (post sources vs the conventions, before the build) validate-site.sh (post-build discoverability checks) + check-plugin-output.rb (each plugin hook's mark on the posts; + run by validate-site.sh) _data/ post_conventions.yml (categories, covers, controlled and retired tags — the one copy, also read by scholar-lens) .github/workflows/ CI: tests → lint → build → html-proofer → validate-site; deploys on push to main + workflow-lint.yml: actionlint and shellcheck ``` **Top-level pages:** `index.html` (home), plus `paper-reviews.md`, @@ -120,14 +123,15 @@ subtitle: "<one-line pitch>" # optional — shown under the title in the h date: YYYY-MM-DD HH:MM:SS paper_author: "<Org>" # the paper's org, not people; omit for Insights/opinion posts seo_title: "<shorter title>" # optional — for <title> when the paper title is too long -description: >- # optional — see below +description: >- # optional, plain text — see below <search-snippet, ~150 chars> categories: ["<Type>", "<Topic>"] tags: ["<Tag-1>", "<Tag-2>"] -cover: /assets/images/<topic>.(jpg|png) +cover: /assets/images/<topic>.(jpg|png) # Insights: insights.jpg use_math: true # ONLY if the post has equations (loads MathJax) lang: ko # required with translation_id (ko | en)… translation_id: <shared-slug> # …links a Korean post to its -en twin; + # set on both (the lint checks the pair); # only the Korean one is listed --- ``` @@ -148,7 +152,16 @@ enough. Write it by hand when the first paragraph opens on a pull quote or a disclosure note — that is, on most Insights posts. Descriptions must be **unique across the site**; CI fails the build if two pages -share one. +share one. They ship as plain text, so the lint rejects `$$`, `*emphasis*`, +backticks, links and tags in them. + +### Dates, file names and Liquid + +The file name starts with the posting day (`YYYY-MM-DD-`); `date:` may be earlier +(a paper's date) but not later. Jekyll renders every post as Liquid first, so a +`{{ … }}` or `{% … %}` in a code block (a prompt template, a GitHub Actions +`${{ … }}`) renders as nothing, or stops the build: wrap it in `{% raw %}…{% endraw %}`. The lint +checks all three. ### Categories drive the URL @@ -159,7 +172,8 @@ Categories are **two levels**: A type with no posts yet keeps its tab and renders an empty-state line: a missing tab reads as a section that was removed, not one still filling up. - `categories[1]` — the **topic**, one of the `topics` in - `_data/post_conventions.yml`, which also gives its cover image if it has one. When none fits, + `_data/post_conventions.yml`, which also gives its cover image if it has one (Insights + posts use the type's `insights.jpg` instead). When none fits, add one there first; the lint rejects any other value. Jekyll slugifies the two and combines them with the date to build the output @@ -216,14 +230,24 @@ The lint also rejects what renders wrong: - math written `\\(…\\)` or `\\[…\\]`, or TeX next to a single `$`: Markdown reads the formula first, as above. -- `\_` inside `$$…$$`: MathJax shows the backslash-underscore literally. Write +- `\_` inside `$$…$$`: MathJax renders a literal underscore, not a subscript. Write `_`; escape it only inside `\text{…}`. - a display `$$…$$` line without a blank line before and after, in lists too: kramdown folds it into the paragraph as inline math. - Hangul inside a `$$` span: almost always two currency `$$`s read as one span. - -Inside inline math, write `\lvert … \rvert` instead of a bare `|`, which a table -reads as a cell break. +- `$$` math in a post without `use_math: true`: MathJax never loads, so the TeX + shows as text. +- a `<name>` that is not HTML, `<s>`/`<input>`, or a bare `<img>` (a model's special token): + kramdown passes it through and the browser drops it. Write it in `code`. +- a code fence with more than a language after the marker (```` ```python title="a" ````), + or one never closed: kramdown does not read it as a fence. +- a tag spelled two ways across posts (`Grouped-Query-…` and `Grouped-query-…`). +- a `|` inside `$$` in a table row: kramdown splits the cell there, and turns + `\|` into `|` (a norm into an absolute value). Write `\lVert … \rVert`. + +Inside inline math, write `\lvert … \rvert` (or `\mid`) instead of a bare `|`, and +`\lVert … \rVert` for a norm; +the lint catches it only in tables. ### Validate before pushing @@ -236,8 +260,9 @@ ruby script/lint-posts.rb # post sources follo bundle exec jekyll build --strict-front-matter \ --destination /tmp/site-verify # does it build clean? bundle exec htmlproofer /tmp/site-verify --disable-external \ - --allow-hash-href --no-enforce-https # broken links, images, anchors? + --allow-hash-href # broken links, images, anchors, http://? script/validate-site.sh /tmp/site-verify # sitemap, feed, metadata, headings +shellcheck script/*.sh assets/images/*.sh && actionlint # workflow-lint.yml, on PRs and main ``` > **If a check reports something impossible, look for a running `jekyll serve` @@ -272,13 +297,9 @@ requests. In order, it: Markdown source, 3. builds the site with `JEKYLL_ENV=production`, 4. runs **html-proofer** over `_site/` (internal links, images, anchors), -5. runs **`script/validate-site.sh`** — sitemap/feed parse at byte 0, every - sitemap URL under the configured `url`, a pinned build timezone, `robots.txt` - not blocking and naming the sitemap, at least one rendered page, exactly one `h1` per page, no - heading-level skips, a description and a canonical on every page, every - description longer than its own title, no duplicate description or title, no - listing page showing a translation beside its original, and no authoring - sources published — and +5. runs **`script/validate-site.sh`** — the post-build checks htmlproofer does + not cover (sitemap, feed, metadata, headings, redirects, plugin output); the + script lists them — and 6. deploys to GitHub Pages. A failure is almost always step 4 or 5; the Actions log names the exact link, diff --git a/_config.yml b/_config.yml index ff17085..1212fde 100644 --- a/_config.yml +++ b/_config.yml @@ -6,7 +6,7 @@ plugins: feed: excerpt_only: true - posts_limit: 50 # comfortably above the post count; excerpts only, so the feed stays small + posts_limit: 50 # above the post count (script/check-plugin-output.rb checks); excerpts only, so the feed stays small # NOTE: the Google/Naver site-verification HTML files are intentionally NOT # excluded — they must be served at the site root, or Search Console / Naver @@ -55,6 +55,10 @@ url: https://bits-bytes-nn.github.io # afternoon-dated posts onto the next day and 404 their live URLs. Pinned to UTC # because every published URL was built that way. timezone: UTC +# Build posts dated later than the build. Posts here are dated 12:00 on their +# day (21:00 KST), so a same-day merge in the Korean morning would deploy +# without the post, and nothing rebuilds the site later. Nothing is scheduled. +future: true # Slugified, so "Paper Reviews" becomes paper-reviews/ rather than the # space-escaped paper%20reviews/ that plain `:categories` produces. Posts first # published under the old form redirect from it — see _plugins/legacy_urls.rb. @@ -69,6 +73,12 @@ logo: /assets/my_logo.png sass: sourcemap: never +# An unknown filter is an error, not a silent pass-through: without this a +# plugin filter that fails to register renders its input (a whole post body +# under "Read time") and every gate stays green. +liquid: + strict_filters: true + markdown: kramdown kramdown: input: GFM diff --git a/_data/post_conventions.yml b/_data/post_conventions.yml index a4827ea..b772f71 100644 --- a/_data/post_conventions.yml +++ b/_data/post_conventions.yml @@ -1,8 +1,9 @@ # Post conventions, as data. # -# The one list of the values a post's front matter may use. script/lint-posts.rb -# checks every post in _posts against it, in CI before the build, and -# scholar-lens reads this same file before it opens a pull request here, so a +# The one list of the values a post's front matter may use, and of the MathJax +# macros head.html defines (math, below). script/lint-posts.rb checks every +# post's front matter against it, in CI before the build, and scholar-lens +# reads this same file before it opens a pull request here, so a # generated post and a hand-written one answer to the same rules. The prose # conventions (README.md "Writing a post", docs/tech-doc.md) point here instead # of repeating the lists. @@ -28,9 +29,14 @@ topics: # Never in front matter. jekyll-feed reads `author` as the entry's author, which # is the site's (see _config.yml); a paper's authors go in `paper_author`. -# `last_modified_at` is set from the file-name date (_plugins/posted_date.rb). +# `last_modified_at` comes from a file-name date later than `date:` +# (_plugins/posted_date.rb). forbidden_keys: [author, last_modified_at] +# `lang` values. A translation is one post in each, sharing a translation_id; +# the language switcher (_includes/language_switcher.html) knows these two. +languages: [ko, en] + math: # The macros _includes/head.html defines for MathJax on top of the standard # set; test/test_lint_posts.rb fails when the two lists differ. diff --git a/_includes/footer.html b/_includes/footer.html index ca77ac9..373f56f 100644 --- a/_includes/footer.html +++ b/_includes/footer.html @@ -51,9 +51,9 @@ <h2 class="footer-heading">{{ site.title }}</h2> <!-- Scripts --> <!-- Code is highlighted server-side by Rouge (theme in _sass/_syntax.scss); no client-side highlighter needed. --> -<!-- GLightbox (vanilla, no jQuery) for image zoom, only on pages that have an +<!-- GLightbox (vanilla, no jQuery) for image zoom, only on posts that have an image to zoom; js/main.js skips the zoom setup when it is absent. --> -{% if content contains '<img' %} +{% if page.layout == 'post' and content contains '<img' %} <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/glightbox@3.3.0/dist/css/glightbox.min.css" integrity="sha384-GPAzSuZc0kFvdIev6wm9zg8gnafE8tLso7rsAYQfc9hAdWCpOcpcNI5W9lWkYcsd" crossorigin="anonymous" /> <script src="https://cdn.jsdelivr.net/npm/glightbox@3.3.0/dist/js/glightbox.min.js" diff --git a/_includes/head.html b/_includes/head.html index f960e11..78a929a 100644 --- a/_includes/head.html +++ b/_includes/head.html @@ -59,11 +59,7 @@ {%- when 'Paper Summaries' -%}{%- assign html_title = html_title | append: ' 논문 요약' -%} {%- endcase -%} {%- endif -%} -{%- if page.url -%} - {%- assign page_full_url = page.url | prepend: site.baseurl | prepend: site.url -%} -{%- else -%} - {%- assign page_full_url = site.url | append: site.baseurl -%} -{%- endif -%} +{%- assign page_full_url = page.url | prepend: site.baseurl | prepend: site.url -%} <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> @@ -153,7 +149,7 @@ <!-- Site styles --> <link rel="stylesheet" href="{{ '/css/main.css' | prepend: site.baseurl }}"> {% if page.stylesheet %}<link rel="stylesheet" href="{{ page.stylesheet | prepend: site.baseurl }}">{% endif %} - <link rel="canonical" href="{{ page.url | replace:'index.html','' | prepend: site.baseurl | prepend: site.url }}" /> + <link rel="canonical" href="{{ page_full_url }}" /> <link rel="alternate" type="application/atom+xml" title="{{ site.title }}" href="{{ '/feed.xml' | prepend: site.baseurl | prepend: site.url }}" /> <!-- Structured data (schema.org JSON-LD) for rich search results --> diff --git a/_layouts/post.html b/_layouts/post.html index 081d91a..0fbfe9f 100644 --- a/_layouts/post.html +++ b/_layouts/post.html @@ -18,8 +18,6 @@ <h1 class="title">{{ page.title }}</h1> </div> <div class="wrapper"> - {% unless page.cover %} {% include page_divider.html %} {% endunless %} - <section class="post-meta"> <div class="post-header-info"> <div class="post-date"> @@ -69,8 +67,8 @@ <h1 class="title">{{ page.title }}</h1> {%- comment -%} adjacent_older / adjacent_newer come from _plugins/related_posts.rb, not Jekyll's page.previous / page.next. The built-ins walk raw date order, and - every translation pair here shares a date — so "← Previous" on the newest - Korean post was that same article in English. + a translation pair sits a second apart, English first — so "← Previous" on a + Korean post would be that same article in English. {%- endcomment -%} {% if page.adjacent_older or page.adjacent_newer %} <nav class="post-nav" aria-label="Adjacent posts"> diff --git a/_plugins/image_dimensions.rb b/_plugins/image_dimensions.rb index 65bee98..c1f63c5 100644 --- a/_plugins/image_dimensions.rb +++ b/_plugins/image_dimensions.rb @@ -33,6 +33,11 @@ def self.read(bytes) end end + # The repo file a root-relative src names. + def self.source_file(src, source, baseurl) + File.join(source, src.delete_prefix(baseurl.to_s)) + end + # `lookup` maps a src path to its bytes, or nil when there is no such file. def self.apply(html, &lookup) html.to_s.gsub(SIZELESS_IMG) do |tag| @@ -74,9 +79,9 @@ def self.svg(root) site = doc.site doc.output = ImageDimensions.apply(doc.output) do |src| - path = File.join(site.source, src.delete_prefix(site.baseurl.to_s)) - # A header is all `read` needs; JPEG SOF can sit past APP segments. - File.file?(path) ? File.binread(path, 64 * 1024) : nil + path = ImageDimensions.source_file(src, site.source, site.baseurl) + # The whole file: a JPEG frame header can sit past large EXIF/XMP segments. + File.file?(path) ? File.binread(path) : nil end end end diff --git a/_plugins/post_description.rb b/_plugins/post_description.rb index ec290ad..002029f 100644 --- a/_plugins/post_description.rb +++ b/_plugins/post_description.rb @@ -28,14 +28,15 @@ module PostDescription MIN_BOUNDARY_FRACTION = 0.6 # Block-level markdown that is never prose: headings, blockquotes, list items, - # tables, fences, raw HTML, and reference definitions. + # tables, fences, block-level raw HTML (a paragraph may open with <strong>), + # and reference definitions. # # The ordered-list alternative is capped at two digits: `\d+[.)]\s` also matched # a Korean date opening ("2024. 5. 14. 공개된 …"), silently discarding the first # paragraph of any post written that way. # `{:` is a kramdown attribute list (`{: .notice}`), which styles the block # next to it and is not text. - SKIP_LINE = /\A(?:\#{1,6}\s|>|[-*+]\s|\d{1,2}[.)]\s|\||```|~~~|<|\[\^?[^\]]+\]:|\{:)/ + SKIP_LINE = /\A(?:\#{1,6}\s|>|[-*+]\s|\d{1,2}[.)]\s|\||```|~~~|<(?!(?:abbr|b|code|em|i|q|strong)\b)|\[\^?[^\]]+\]:|\{:)/ # A line of `-` or `=` under text makes that text a (setext) heading; on its # own, `---`/`***`/`___` is a rule. Neither is prose. @@ -49,11 +50,15 @@ module PostDescription # one emphasis really spans the paragraph — not on prose that merely begins and # ends with emphasis ("**핵심**은 데이터입니다. 그래서 **중요합니다**"). Only the # delimiter that opened is excluded, so a note naming `snake_case` still counts. - WHOLLY_EMPHASISED = /\A(?:(\*{1,2})[^*]+\1|(_{1,2})[^_]+\2)\z/m + WHOLLY_EMPHASISED = %r{\A(?:(\*{1,2})[^*]+\1|(_{1,2})[^_]+\2|<(strong|b|em|i)>(?:(?!</\3>).)+</\3>)\z}m + + # Comments are notes to the author; their inner lines read as prose. Raw tags + # are Liquid, which this hook runs before. + COMMENTS = %r{<!--.*?-->|\{::comment\}.*?\{:/comment\}|\{%-?\s*(?:end)?raw\s*-?%\}}m class << self def derive(markdown, limit = LIMIT) - para = first_prose_paragraph(markdown.to_s) + para = first_prose_paragraph(markdown.to_s.gsub(COMMENTS, "")) return "" if para.nil? truncate(strip_inline(para), limit) @@ -134,7 +139,7 @@ def first_prose_paragraph(markdown) # summary of the post. def accept(lines) candidate = lines.join(" ").strip - return nil if strip_inline(candidate).empty? + return nil if strip_inline(candidate.gsub(/\$\$.*?\$\$/m, " ")).strip.empty? return nil if candidate.match?(WHOLLY_EMPHASISED) candidate @@ -142,9 +147,13 @@ def accept(lines) def strip_inline(text) out = text.dup - out.gsub!(/\$\$.*?\$\$/m, " ") # display math - out.gsub!(/\\\((.*?)\\\)/m, " ") # inline math - out.gsub!(/`([^`]*)`/, '\1') # code spans + # Math stays when it reads as text ("O(n^2)"). A TeX command would not, + # nor would a brace group, and `*`, `_`, `<` or `>` would be taken for + # markup below. + out.gsub!(/\$\$(.*?)\$\$/m) { Regexp.last_match(1).match?(/[\\*_<>{}]/) ? " " : Regexp.last_match(1) } + # Code spans and backslash escapes are text: hide them from the passes below. + hide = ->(text) { text.gsub(/[\\`*_{}\[\]()#+\-.!<>$|~]/) { |c| (0xE000 + c.ord).chr(Encoding::UTF_8) } } + out.gsub!(/`([^`]*)`/) { hide.call(Regexp.last_match(1)) } out.gsub!(/!\[[^\]]*\]\([^)]*\)/, " ") # images out.gsub!(/\[([^\]]*)\]\([^)]*\)/, '\1') # links -> link text out.gsub!(/\[\^[^\]]+\]/, "") # footnote references @@ -152,6 +161,7 @@ def strip_inline(text) # directly to the emphasised word, so "<em>16.7%</em>였습니다" must not # become "16.7% 였습니다". out.gsub!(%r{<br\s*/?>}i, " ") + out.gsub!(/\\([\\`*_{}\[\]()#+\-.!<>$|~])/) { hide.call(Regexp.last_match(1)) } out.gsub!(/<[^>]+>/, "") # inline HTML out.gsub!(/(\*\*|__)(.*?)\1/m, '\2') # bold out.gsub!(/\*(?=\S)(.*?)(?<=\S)\*/m, '\1') # italic @@ -160,6 +170,7 @@ def strip_inline(text) # not have one: Korean attaches particles directly ("*강조*된"). out.gsub!(/(?<![[:alnum:]])_(?=\S)(.*?)(?<=\S)_(?![[:alnum:]])/m, '\1') out.gsub!(/~~(.*?)~~/m, '\1') # strikethrough + out.gsub!(/[\uE000-\uE07F]/) { |c| (c.ord - 0xE000).chr } out.gsub(/\s+/, " ").strip end end diff --git a/_plugins/related_posts.rb b/_plugins/related_posts.rb index 590e2c7..45f48f7 100644 --- a/_plugins/related_posts.rb +++ b/_plugins/related_posts.rb @@ -9,8 +9,8 @@ # Both answers obey the same two eligibility rules (`eligible?`), which is why # they live together: never point a reader at this post's own translation, and # never cross languages. Jekyll's built-in `page.previous`/`page.next` know -# neither rule, and a translation pair shares a date — so a Korean post's -# "← Previous" would be its own English version. +# neither rule, and a translation pair sits a second apart, English first — so +# a Korean post's "← Previous" would be its own English version. # # A shared topic tag is required. Sharing only a category is not a reading # recommendation: a broad subcategory like "Paper Reviews / Language-Models" @@ -131,7 +131,7 @@ def self.sort_time(post) end by_url = docs.to_h { |d| [d.url, d] } # Sorted here rather than trusting the collection's order, and by id as well - # as date because a translation pair shares its date exactly. + # as date so posts with the same timestamp keep one order on every build. ordered = meta.sort_by { |m| [RelatedPosts.sort_time(m), m[:id].to_s] } meta.each_with_index do |target, i| diff --git a/_posts/2025-01-17-from-local-to-global-a-graph-rag-approach-to-query-focused-summarization.md b/_posts/2025-01-17-from-local-to-global-a-graph-rag-approach-to-query-focused-summarization.md index 514c162..750ba61 100644 --- a/_posts/2025-01-17-from-local-to-global-a-graph-rag-approach-to-query-focused-summarization.md +++ b/_posts/2025-01-17-from-local-to-global-a-graph-rag-approach-to-query-focused-summarization.md @@ -4,7 +4,7 @@ title: "From Local to Global: A Graph RAG Approach to Query-Focused Summarizatio date: 2024-04-24 18:38:11 paper_author: "Microsoft Research" description: >- - Graph RAG는 LLM으로 엔티티 지식 그래프와 커뮤니티 요약을 미리 만들어 전역 질문에 맵-리듀스로 답합니다. 기존 RAG보다 포괄성과 다양성에서 앞섰고, 루트 레벨 요약은 원문 요약보다 토큰을 97% 덜 씁니다. 색인 구축과 평가를 정리합니다. + Graph RAG는 LLM으로 엔티티 지식 그래프와 커뮤니티 요약을 미리 만들어 전역 질문에 맵-리듀스로 답합니다. 기존 RAG보다 포괄성과 다양성에서 앞섰고, 루트 레벨 요약은 원문 요약보다 토큰을 97% 이상 덜 씁니다. 색인 구축과 평가를 정리합니다. categories: ["Paper Reviews", "Retrieval-Augmented-Generation"] tags: ["GraphRAG", "Query-Focused-Summarization", "Community-Based-Summarization", "Entity-and-Relationship-Extraction", "Map-Reduce-Summarization", "Modularity-Based-Retrieval", "Community-Detection", "Leiden-Algorithm-for-Text", "Multi-Level-Community-Indexing", "Global-Context-Understanding", "Retrieval-Augmented-Generation", "Knowledge-Graph"] cover: /assets/images/retrieval-augmented-generation.jpg @@ -21,7 +21,7 @@ use_math: true 구현은 크게 네 단계로 이루어집니다. 먼저 소스 문서를 의미 있는 청크로 분할하고, LLM을 사용하여 엔티티와 관계를 추출합니다. 다음으로 Leiden 알고리즘을 사용하여 그래프의 커뮤니티 구조를 탐지하고, 각 커뮤니티에 대한 요약을 생성합니다. 마지막으로 사용자 쿼리가 주어지면 관련 커뮤니티 요약들을 활용하여 맵-리듀스 방식으로 최종 답변을 생성합니다. 실험은 약 100만 토큰 규모의 두 데이터셋(팟캐스트 전사본과 뉴스 기사)에서 수행되었으며, 다양한 커뮤니티 레벨과 컨텍스트 윈도우 크기에 대한 철저한 평가가 이루어졌습니다. ### 이 연구의 결과가 가지는 의미는 무엇입니까? -실험 결과는 그래프 RAG가 기존 RAG 시스템과 비교하여 답변의 포괄성(72-83%)과 다양성(62-82%) 측면에서 상당한 개선을 달성했음을 보여줍니다. 특히 주목할 만한 점은 효율성입니다. 루트 레벨 커뮤니티 요약은 소스 텍스트 요약보다 97% 더 적은 토큰을 사용하면서도 경쟁력 있는 성능을 유지했습니다. 이는 대규모 문서 컬렉션에 대한 의미 파악과 전역적 이해를 위한 실용적인 솔루션의 가능성을 제시합니다. 또한 오픈소스로 공개될 예정인 이 구현은 향후 그래프 기반 RAG 시스템의 발전을 위한 중요한 기반이 될 것으로 기대됩니다. +실험 결과는 그래프 RAG가 기존 RAG 시스템과 비교하여 답변의 포괄성(72-83%)과 다양성(62-82%) 측면에서 상당한 개선을 달성했음을 보여줍니다. 특히 주목할 만한 점은 효율성입니다. 루트 레벨 커뮤니티 요약은 소스 텍스트 요약보다 97% 이상 적은 토큰을 사용하면서도 경쟁력 있는 성능을 유지했습니다. 이는 대규모 문서 컬렉션에 대한 의미 파악과 전역적 이해를 위한 실용적인 솔루션의 가능성을 제시합니다. 또한 오픈소스로 공개될 예정인 이 구현은 향후 그래프 기반 RAG 시스템의 발전을 위한 중요한 기반이 될 것으로 기대됩니다. - - - ## 로컬에서 글로벌로: 쿼리 중심 요약을 위한 그래프 RAG 접근법 @@ -478,4 +478,4 @@ LLM과 RAG와 관련된 그래프 활용은 여러 방향으로 발전하고 있 - - - ### References -* [From Local to Global: A Graph RAG Approach to Query-Focused Summarization](http://arxiv.org/pdf/2404.16130v1) +* [From Local to Global: A Graph RAG Approach to Query-Focused Summarization](https://arxiv.org/pdf/2404.16130v1) diff --git a/_posts/2025-01-27-llama-2-open-foundation-and-fine-tuned-chat-models.md b/_posts/2025-01-27-llama-2-open-foundation-and-fine-tuned-chat-models.md index a519819..edd55f3 100644 --- a/_posts/2025-01-27-llama-2-open-foundation-and-fine-tuned-chat-models.md +++ b/_posts/2025-01-27-llama-2-open-foundation-and-fine-tuned-chat-models.md @@ -524,5 +524,5 @@ AI 모델을 사용하는 모든 사람이 선의를 가지고 있는 것은 아 - - - ### References -* [Llama 2: Open Foundation and Fine-Tuned Chat Models](http://arxiv.org/pdf/2307.09288v2) +* [Llama 2: Open Foundation and Fine-Tuned Chat Models](https://arxiv.org/pdf/2307.09288v2) * [Llama Models (GitHub)](https://github.com/meta-llama/llama-models) diff --git a/_posts/2025-01-28-llama-open-and-efficient-foundation-language-models.md b/_posts/2025-01-28-llama-open-and-efficient-foundation-language-models.md index 03e1cc1..bd75103 100644 --- a/_posts/2025-01-28-llama-open-and-efficient-foundation-language-models.md +++ b/_posts/2025-01-28-llama-open-and-efficient-foundation-language-models.md @@ -4,7 +4,7 @@ title: "LLaMA: Open and Efficient Foundation Language Models" date: 2023-02-27 17:11:15 paper_author: "Meta AI" description: >- - LLaMA는 공개 데이터만으로 7B~65B 모델을 1.4T 토큰까지 학습해, 13B 모델이 GPT-3(175B)를 앞서고 65B 모델이 PaLM-540B와 견줄 성능을 냈습니다. 데이터 구성, 아키텍처 개선과 학습 효율화 방법을 정리합니다. + LLaMA는 공개 데이터만으로 7B~65B 모델을 1.4T 토큰까지 학습해, 13B 모델이 대부분의 벤치마크에서 GPT-3(175B)를 앞서고 65B 모델이 PaLM-540B와 견줄 성능을 냈습니다. 데이터 구성, 아키텍처 개선과 학습 효율화 방법을 정리합니다. categories: ["Paper Reviews", "Language-Models"] tags: ["Rotary-Positional-Embeddings", "SwiGLU-Activation-Function", "Scaling-Laws", "Economical-Training", "Computation-Communication-Overlap", "Public-Dataset-Only-Training", "RMSNorm", "Efficient-Attention", "CommonCrawl-Quality-Filtering", "Llama"] cover: /assets/images/language-models.jpg @@ -22,7 +22,7 @@ LLaMA는 기존 접근법과 달리 모델 크기와 학습 데이터 양 사이 LLaMA의 구현은 최신 트랜스포머 아키텍처를 기반으로 하되, 여러 가지 효율적인 최적화 기법을 도입했습니다. 사전 정규화, SwiGLU 활성화 함수, RoPE(회전 위치 임베딩) 등을 적용하여 모델의 성능과 학습 효율성을 개선했습니다. 학습 과정에서는 AdamW 옵티마이저와 코사인 학습률 스케줄링을 사용했으며, 효율적인 구현을 위해 xformers 라이브러리의 최적화된 어텐션 메커니즘을 활용했습니다. 특히 2048개의 A100 GPU를 사용하여 GPU당 약 380 토큰/초의 처리 속도를 달성했으며, 이를 통해 1.4T 토큰의 학습을 약 21일 만에 완료할 수 있었습니다. ### 이 연구의 결과가 가지는 의미는 무엇입니까? -이 연구의 가장 중요한 의의는 공개 데이터만으로도 최고 수준의 언어 모델을 개발할 수 있다는 것을 입증했다는 점입니다. 특히 13B 파라미터 모델이 GPT-3(175B)를 능가하고, 65B 모델이 PaLM-540B와 견줄만한 성능을 보였다는 것은 매우 중요한 발견입니다. 이는 효율적인 모델 설계와 데이터 선택이 단순한 모델 크기 확장보다 더 중요할 수 있다는 것을 시사합니다. 또한 작은 규모의 모델들이 단일 GPU에서도 실행 가능하다는 점은 LLM 연구의 민주화에 크게 기여할 것으로 기대됩니다. 다만 모델이 여전히 편향성과 유해성을 보이는 것으로 나타나, 이러한 문제들의 해결을 위한 추가 연구의 필요성도 제기되었습니다. +이 연구의 가장 중요한 의의는 공개 데이터만으로도 최고 수준의 언어 모델을 개발할 수 있다는 것을 입증했다는 점입니다. 특히 13B 파라미터 모델이 대부분의 벤치마크에서 GPT-3(175B)를 능가하고, 65B 모델이 PaLM-540B와 견줄만한 성능을 보였다는 것은 매우 중요한 발견입니다. 이는 효율적인 모델 설계와 데이터 선택이 단순한 모델 크기 확장보다 더 중요할 수 있다는 것을 시사합니다. 또한 작은 규모의 모델들이 단일 GPU에서도 실행 가능하다는 점은 LLM 연구의 민주화에 크게 기여할 것으로 기대됩니다. 다만 모델이 여전히 편향성과 유해성을 보이는 것으로 나타나, 이러한 문제들의 해결을 위한 추가 연구의 필요성도 제기되었습니다. - - - ## LLaMA: 개방형 고효율 파운데이션 언어 모델 @@ -246,5 +246,5 @@ GPT-3는 1,750억 파라미터를 가진 모델로 중요한 돌파구를 마련 - - - ### References -* [LLaMA: Open and Efficient Foundation Language Models](http://arxiv.org/pdf/2302.13971v1) +* [LLaMA: Open and Efficient Foundation Language Models](https://arxiv.org/pdf/2302.13971v1) diff --git a/_posts/2025-02-03-deepseek-llm-scaling-open-source-language-models-with-longtermism.md b/_posts/2025-02-03-deepseek-llm-scaling-open-source-language-models-with-longtermism.md index 2faaa6b..9c7f093 100644 --- a/_posts/2025-02-03-deepseek-llm-scaling-open-source-language-models-with-longtermism.md +++ b/_posts/2025-02-03-deepseek-llm-scaling-open-source-language-models-with-longtermism.md @@ -279,5 +279,5 @@ DeepSeek LLM의 안전성 평가를 위해 다양한 분야의 전문가 20명 - - - ### References -* [DeepSeek LLM: Scaling Open-Source Language Models with Longtermism](http://arxiv.org/pdf/2401.02954v1) +* [DeepSeek LLM: Scaling Open-Source Language Models with Longtermism](https://arxiv.org/pdf/2401.02954v1) * [DeepSeek-LLM (GitHub)](https://github.com/deepseek-ai/DeepSeek-LLM) diff --git a/_posts/2025-02-05-deepseekmoe-towards-ultimate-expert-specialization-in-mixture-of-experts-language-models.md b/_posts/2025-02-05-deepseekmoe-towards-ultimate-expert-specialization-in-mixture-of-experts-language-models.md index a843922..3bcb6ba 100644 --- a/_posts/2025-02-05-deepseekmoe-towards-ultimate-expert-specialization-in-mixture-of-experts-language-models.md +++ b/_posts/2025-02-05-deepseekmoe-towards-ultimate-expert-specialization-in-mixture-of-experts-language-models.md @@ -343,4 +343,4 @@ MoE 아키텍처와 학습 전략에 대한 연구 외에도, 최근에는 기 - - - ### References -* [DeepSeekMoE: Towards Ultimate Expert Specialization in Mixture-of-Experts Language Models](http://arxiv.org/pdf/2401.06066v1) +* [DeepSeekMoE: Towards Ultimate Expert Specialization in Mixture-of-Experts Language Models](https://arxiv.org/pdf/2401.06066v1) diff --git a/_posts/2025-02-14-deepseek-coder-when-the-large-language-model-meets-programming-the-rise-of-code-intelligence.md b/_posts/2025-02-14-deepseek-coder-when-the-large-language-model-meets-programming-the-rise-of-code-intelligence.md index a75fda5..360d963 100644 --- a/_posts/2025-02-14-deepseek-coder-when-the-large-language-model-meets-programming-the-rise-of-code-intelligence.md +++ b/_posts/2025-02-14-deepseek-coder-when-the-large-language-model-meets-programming-the-rise-of-code-intelligence.md @@ -5,7 +5,7 @@ seo_title: "DeepSeek-Coder: The Rise of Code Intelligence" # <title> only; the date: 2024-01-25 14:17:53 paper_author: "DeepSeek AI" description: >- - DeepSeek-Coder는 87개 프로그래밍 언어의 2조 토큰을 저장소 단위로 구성하고 FIM과 16K 컨텍스트로 1.3B~33B 코드 모델을 학습했습니다. Instruct 33B는 GPT-3.5 Turbo를 앞섰습니다. 데이터 구축과 학습 전략을 정리합니다. + DeepSeek-Coder는 87개 프로그래밍 언어의 2조 토큰을 저장소 단위로 구성하고 FIM과 16K 컨텍스트로 1.3B~33B 코드 모델을 학습했습니다. Instruct 33B는 대부분의 벤치마크에서 GPT-3.5 Turbo를 앞섰습니다. 데이터 구축과 학습 전략을 정리합니다. categories: ["Paper Reviews", "Language-Models"] tags: ["Repository-Level-Data-Construction", "Fill-in-the-Middle-Code-Completion", "Cross-File-Code-Completion", "Long-Context-Adaptation", "Repository-Level-Deduplication", "Dependency-Aware-Training", "Project-Level-Code-Understanding", "Cross-File-Dependency-Analysis", "Topological-Sorting-for-Code-Learning", "LeetCode-Competition-Benchmark", "DeepSeek"] cover: /assets/images/language-models.jpg @@ -22,7 +22,7 @@ DeepSeek-Coder는 세 가지 핵심적인 기술적 혁신을 도입했습니다 DeepSeek-Coder는 1.3B부터 33B 파라미터에 이르는 다양한 규모의 모델을 제공합니다. 학습 데이터는 소스 코드 87%, 영어 코드 관련 자연어 코퍼스 10%, 중국어 자연어 코퍼스 3%로 구성되었습니다. 데이터 구축 과정에서는 크롤링, 규칙 기반 필터링, 의존성 분석, 저장소 수준의 중복 제거, 품질 검사 단계를 거쳤습니다. 특히 의존성 분석을 통해 파일 간의 호출 관계를 고려한 학습 데이터를 구성했으며, Fill-In-Middle 학습 전략을 통해 코드의 전체적인 구조와 흐름을 이해하도록 했습니다. ### 이 연구의 결과가 가지는 의미는 무엇입니까? -DeepSeek-Coder는 다양한 벤치마크에서 기존의 오픈소스 코드 모델들을 능가하는 성능을 보여주었으며, 특히 DeepSeek-Coder-Instruct 33B 모델은 GPT-3.5 Turbo의 성능을 뛰어넘는 결과를 달성했습니다. 더욱 주목할 만한 점은 7B 규모의 모델이 33B 규모의 다른 모델들과 비교해도 경쟁력 있는 성능을 보여주었다는 것입니다. 이는 모델 크기보다 효율적인 학습 방법과 데이터 구성이 더 중요할 수 있다는 점을 시사합니다. 또한 이 연구는 코드 인텔리전스 분야에서 오픈소스 모델의 가능성을 입증하고, 향후 연구 방향을 제시했다는 점에서 큰 의미를 가집니다. +DeepSeek-Coder는 다양한 벤치마크에서 기존의 오픈소스 코드 모델들을 능가하는 성능을 보여주었으며, 특히 DeepSeek-Coder-Instruct 33B 모델은 대부분의 벤치마크에서 GPT-3.5 Turbo의 성능을 뛰어넘는 결과를 달성했습니다. 더욱 주목할 만한 점은 7B 규모의 모델이 33B 규모의 다른 모델들과 비교해도 경쟁력 있는 성능을 보여주었다는 것입니다. 이는 모델 크기보다 효율적인 학습 방법과 데이터 구성이 더 중요할 수 있다는 점을 시사합니다. 또한 이 연구는 코드 인텔리전스 분야에서 오픈소스 모델의 가능성을 입증하고, 향후 연구 방향을 제시했다는 점에서 큰 의미를 가집니다. - - - ## DeepSeek-Coder: 코드 인텔리전스의 부상과 대규모 언어 모델의 만남 @@ -274,4 +274,4 @@ DeepSeek-Coder-Base 모델의 학습 과정에서 성능 변화를 추적하기 - - - ### References -* [DeepSeek-Coder: When the Large Language Model Meets Programming -- The Rise of Code Intelligence](http://arxiv.org/pdf/2401.14196v2) +* [DeepSeek-Coder: When the Large Language Model Meets Programming -- The Rise of Code Intelligence](https://arxiv.org/pdf/2401.14196v2) diff --git a/_posts/2025-02-14-raptor-recursive-abstractive-processing-for-tree-organized-retrieval.md b/_posts/2025-02-14-raptor-recursive-abstractive-processing-for-tree-organized-retrieval.md index 14da948..99dc98e 100644 --- a/_posts/2025-02-14-raptor-recursive-abstractive-processing-for-tree-organized-retrieval.md +++ b/_posts/2025-02-14-raptor-recursive-abstractive-processing-for-tree-organized-retrieval.md @@ -4,7 +4,7 @@ title: "RAPTOR: Recursive Abstractive Processing for Tree-Organized Retrieval" date: 2024-01-31 18:30:21 paper_author: "Stanford University" description: >- - RAPTOR는 텍스트 청크를 재귀적으로 임베딩·클러스터링·요약해 여러 추상화 수준의 트리를 만들고 검색에 씁니다. GPT-4와 결합해 QuALITY에서 정확도를 절대 20% 높였습니다. 트리 구축과 축소된 트리 검색 방식을 정리합니다. + RAPTOR는 텍스트 청크를 재귀적으로 임베딩·클러스터링·요약해 여러 추상화 수준의 트리를 만들고 검색에 씁니다. GPT-4와 결합해 QuALITY에서 기존 최고 정확도를 20%포인트 높였습니다. 트리 구축과 축소된 트리 검색 방식을 정리합니다. categories: ["Paper Reviews", "Retrieval-Augmented-Generation"] tags: ["Tree-Organized-Retrieval", "Hierarchical-Summarization", "Collapsed-Tree-Retrieval", "Dependency-Aware-Tree-Traversal", "Multi-Level-Abstraction-Retrieval", "Gaussian-Mixture-Model-Clustering", "Semantic-Similarity-Clustering", "Contextual-Flexibility-Retrieval", "Retrieval-Augmented-Generation"] cover: /assets/images/retrieval-augmented-generation.jpg @@ -246,4 +246,4 @@ RAPTOR 모델 내 요약의 품질과 정확성을 평가하기 위해 생성된 - - - ### References -* [RAPTOR: Recursive Abstractive Processing for Tree-Organized Retrieval](http://arxiv.org/pdf/2401.18059v1) +* [RAPTOR: Recursive Abstractive Processing for Tree-Organized Retrieval](https://arxiv.org/pdf/2401.18059v1) diff --git a/_posts/2025-02-27-mistral-7b.md b/_posts/2025-02-27-mistral-7b.md index de29ac9..42d7ef7 100644 --- a/_posts/2025-02-27-mistral-7b.md +++ b/_posts/2025-02-27-mistral-7b.md @@ -4,7 +4,7 @@ title: "Mistral 7B" date: 2023-10-10 17:54:58 paper_author: "Mistral AI" description: >- - Mistral 7B는 그룹 쿼리 어텐션과 슬라이딩 윈도우 어텐션으로 모든 벤치마크에서 Llama 2 13B를, 추론·수학·코드에서 Llama 1 34B를 앞섭니다. 롤링 버퍼 캐시와 청킹을 포함한 아키텍처와 평가 결과를 정리합니다. + Mistral 7B는 그룹 쿼리 어텐션과 슬라이딩 윈도우 어텐션을 갖춰, 거의 모든 벤치마크에서 Llama 2 13B를, 추론·수학·코드에서 Llama 1 34B를 앞섭니다. 롤링 버퍼 캐시와 청킹을 포함한 아키텍처와 평가 결과를 정리합니다. categories: ["Paper Reviews", "Language-Models"] tags: ["Grouped-Query-Attention", "Sliding-Window-Attention", "Rolling-Buffer-Cache", "Prefill-and-Chunking", "Self-Reflection-Content-Moderation", "System-Prompt-Guardrails", "Token-Efficient-Knowledge-Compression", "Performance-Training-Inference-Tradeoff", "Scaling-Laws", "Efficient-Attention"] cover: /assets/images/language-models.jpg @@ -289,5 +289,5 @@ Mistral 7B의 연구는 언어 모델 개발에 있어 새로운 패러다임을 - - - ### References -* [Mistral 7B](http://arxiv.org/pdf/2310.06825v1) +* [Mistral 7B](https://arxiv.org/pdf/2310.06825v1) * [Mistral Inference (GitHub)](https://github.com/mistralai/mistral-inference) diff --git a/_posts/2025-02-28-deepseekmath-pushing-the-limits-of-mathematical-reasoning-in-open-language-models.md b/_posts/2025-02-28-deepseekmath-pushing-the-limits-of-mathematical-reasoning-in-open-language-models.md index 0cd6e43..87d36d2 100644 --- a/_posts/2025-02-28-deepseekmath-pushing-the-limits-of-mathematical-reasoning-in-open-language-models.md +++ b/_posts/2025-02-28-deepseekmath-pushing-the-limits-of-mathematical-reasoning-in-open-language-models.md @@ -639,4 +639,4 @@ $$ GC_{GRPO}(q,o,t,\pi_{\theta_{rm}}) = \hat{A}_{i,t}+\beta\left(\frac{\pi_{ref} - - - ### References -* [DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models](http://arxiv.org/pdf/2402.03300v3) +* [DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models](https://arxiv.org/pdf/2402.03300v3) diff --git a/_posts/2025-04-04-mixtral-of-experts.md b/_posts/2025-04-04-mixtral-of-experts.md index 3cf86eb..eb94f52 100644 --- a/_posts/2025-04-04-mixtral-of-experts.md +++ b/_posts/2025-04-04-mixtral-of-experts.md @@ -5,7 +5,7 @@ subtitle: "Mixtral of Experts: 전문가 혼합 모델의 새로운 지평" date: 2024-01-08 18:47:34 paper_author: "Mistral AI" description: >- - Mixtral 8x7B는 층마다 8개 전문가 중 2개를 고르는 희소 전문가 혼합 모델로, 총 47B 중 13B만 활성화하고도 대부분의 벤치마크에서 Llama 2 70B와 GPT-3.5에 맞먹습니다. 라우팅 구조와 학습 과정을 정리합니다. + Mixtral 8x7B는 층마다 8개 전문가 중 2개를 고르는 희소 전문가 혼합 모델로, 총 47B 중 13B만 활성화하고도 대부분의 벤치마크에서 Llama 2 70B·GPT-3.5와 같거나 앞섭니다. 라우팅 구조와 학습 과정을 정리합니다. categories: ["Paper Reviews", "Language-Models"] tags: ["Mixture-of-Experts", "Dynamic-Expert-Routing", "Two-Expert-Token-Processing", "Efficient-Inference", "Routing-Network-Token-Selection", "Expert-Selection-Locality-Analysis", "Multilingual-Performance-Scaling", "Long-Context-Retrieval-Optimization", "Instruction-Fine-Tuning", "Direct-Preference-Optimization"] cover: /assets/images/language-models.jpg @@ -340,5 +340,5 @@ Mixtral의 성공은 희소 전문가 혼합 모델이 대규모 언어 모델 - - - ### References -* [Mixtral of Experts](http://arxiv.org/pdf/2401.04088v1) +* [Mixtral of Experts](https://arxiv.org/pdf/2401.04088v1) * [Mistral Inference (GitHub)](https://github.com/mistralai/mistral-inference) diff --git a/_posts/2025-10-03-gemma-3-technical-report.md b/_posts/2025-10-03-gemma-3-technical-report.md index 2000c08..351911d 100644 --- a/_posts/2025-10-03-gemma-3-technical-report.md +++ b/_posts/2025-10-03-gemma-3-technical-report.md @@ -4,7 +4,7 @@ title: "Gemma 3 Technical Report" date: 2025-03-25 15:52:34 paper_author: "Google DeepMind" description: >- - Gemma 3는 로컬 어텐션 5개마다 글로벌 어텐션 1개를 두어 128K 컨텍스트의 메모리 부담을 줄였고, Chatbot Arena에서 Elo 1,338점으로 전체 9위에 올랐습니다. 멀티모달 처리와 지식 증류 기반 학습 방법을 정리합니다. + Gemma 3는 로컬 어텐션 5개마다 글로벌 어텐션 1개를 두어 128K 컨텍스트의 메모리 부담을 줄였고, 27B 모델은 Chatbot Arena에서 Elo 1,338점으로 공동 9위에 올랐습니다. 멀티모달 처리와 지식 증류 기반 학습 방법을 정리합니다. categories: ["Paper Reviews", "Multimodal-Learning"] tags: ["Interleaved-Local-Global-Attention", "Long-Context-Adaptation", "Pan-and-Scan-Image-Processing", "Vision-Encoder-Token-Condensation", "Multimodal-Knowledge-Distillation", "RoPE-Positional-Embedding-Extension", "Grouped-Query-Attention", "Quantization-Aware-Training", "Sliding-Window-Attention", "Efficient-Attention", "Multimodal-Models", "Quantization"] cover: /assets/images/multimodal-learning.jpg @@ -31,7 +31,7 @@ Gemma 3의 구현은 지식 증류와 혁신적인 후훈련 레시피를 중심 ### 이 연구의 결과가 가지는 의미는 무엇입니까? -Gemma 3의 성과는 AI 기술의 실용성과 접근성 측면에서 매우 중요한 의미를 갖습니다. LMSYS Chatbot Arena에서 1,338점의 Elo 점수를 기록하며 전체 순위 9위에 올랐고, 405B 매개변수를 가진 LLaMA 3.1 모델을 능가하는 성능을 보였습니다. 특히 27B 모델이 Gemini-1.5-Pro와 비교 가능한 수준의 성능을 달성한 것은 매개변수 효율성 측면에서 획기적인 성과입니다. +Gemma 3의 성과는 AI 기술의 실용성과 접근성 측면에서 매우 중요한 의미를 갖습니다. 27B 지시 조정 모델은 LMSYS Chatbot Arena에서 1,338점의 Elo 점수를 기록하며 공동 9위(상위 10위권)에 올랐고, 405B 매개변수를 가진 LLaMA 3.1 모델을 능가하는 성능을 보였습니다. 특히 27B 모델이 Gemini-1.5-Pro와 비교 가능한 수준의 성능을 달성한 것은 매개변수 효율성 측면에서 획기적인 성과입니다. 더욱 주목할 만한 점은 모델의 안전성과 프라이버시 보호 측면입니다. 기존 모델들과 비교하여 현저히 낮은 기억화 비율을 보였으며, 기억화된 출력에서 개인정보가 전혀 관찰되지 않았습니다. 이는 오픈 소스 모델의 안전한 배포 가능성을 크게 높이는 중요한 성과입니다. 또한 ShieldGemma 2와 같은 안전 분류기 개발을 통해 AI 기술의 책임감 있는 활용 가능성을 보여주었습니다. - - - @@ -399,7 +399,7 @@ $$P(m \text{ beats } m') = \frac{e^{\xi_m}}{e^{\xi_m} + e^{\xi_{m'}}}$$ 여기서 $$\xi_m$$과 $$\xi_{m'}$$는 각각 모델 $$m$$과 $$m'$$의 Bradley-Terry 계수입니다. 이 모델은 인간 평가자들의 선호도 데이터를 바탕으로 최대우도추정을 통해 각 모델의 상대적 성능을 정량화합니다. -Chatbot Arena의 평가 결과를 보면, **Gemma 3 27B IT 모델은 1338점의 Elo 점수**를 달성하여 전체 순위에서 9위를 기록했습니다. 이는 매우 인상적인 결과로, 다음과 같은 중요한 의미를 갖습니다. +Chatbot Arena의 평가 결과를 보면, **Gemma 3 27B IT 모델은 1338점의 Elo 점수**를 달성하여 공동 9위를 기록했습니다. 이는 매우 인상적인 결과로, 다음과 같은 중요한 의미를 갖습니다. **오픈 모델 중 최상위 성능**: Gemma 3 27B IT는 DeepSeek-V3 (1318점), Meta-Llama-3.1-405B-Instruct (1269점), Qwen2.5-72B-Instruct (1257점)와 같은 훨씬 큰 규모의 오픈 모델들을 능가했습니다. 특히 405B 매개변수를 가진 LLaMA 3.1 모델보다 높은 점수를 기록한 것은 Gemma 3의 효율성을 보여주는 중요한 지표입니다. @@ -412,7 +412,7 @@ Chatbot Arena의 평가 결과를 보면, **Gemma 3 27B IT 모델은 1338점의 | 순위 | 모델 | Elo 점수 | 95% 신뢰구간 | 오픈 | 타입 | 매개변수 | | ---- | ------------------------- | -------- | ------------ | ------ | --------- | -------- | | 1 | Grok-3-Preview | 1412 | +8/-10 | - | - | - | -| 2 | GPT-4.5-Preview | 1411 | +11/-11 | - | - | - | +| 1 | GPT-4.5-Preview | 1411 | +11/-11 | - | - | - | | 3 | Gemini-2.0-Flash-Thinking | 1384 | +6/-5 | - | - | - | | 6 | DeepSeek-R1 | 1363 | +8/-6 | 예 | MoE | 671B/37B | | 9 | **Gemma-3-27B-IT** | **1338** | **+8/-9** | **예** | **Dense** | **27B** | @@ -669,7 +669,7 @@ Gemma 3 모델들이 **표준 소비자급 하드웨어에서 실행되도록 ### 성능 평가와 벤치마크 결과 -LMSYS Chatbot Arena에서의 성과는 Gemma 3의 실질적 성능을 입증하는 중요한 지표입니다. **Gemma 3 27B IT 모델의 1338점 Elo 점수**는 전체 순위 9위에 해당하며, 405B 매개변수를 가진 LLaMA 3.1 모델(1257점)을 능가하는 결과입니다. 이는 매개변수 효율성 측면에서 탁월한 성능을 보여주는 것으로, 모델 크기 대비 성능 최적화의 새로운 기준을 제시합니다. +LMSYS Chatbot Arena에서의 성과는 Gemma 3의 실질적 성능을 입증하는 중요한 지표입니다. **Gemma 3 27B IT 모델의 1338점 Elo 점수**는 공동 9위에 해당하며, 405B 매개변수를 가진 LLaMA 3.1 모델(1269점)을 능가하는 결과입니다. 이는 매개변수 효율성 측면에서 탁월한 성능을 보여주는 것으로, 모델 크기 대비 성능 최적화의 새로운 기준을 제시합니다. 또한 다양한 표준 벤치마크에서의 일관된 성능 향상은 Gemma 3가 특정 작업에 특화된 것이 아니라 범용적인 능력 향상을 달성했음을 보여줍니다. 수학, 코딩, 추론, 다국어 능력 등 모든 영역에서의 개선은 종합적인 AI 시스템으로서의 가치를 입증합니다. @@ -841,4 +841,4 @@ Gemma 3 모델들의 다국어 능력 평가 결과는 다음과 같습니다. 이러한 상세한 평가 결과들은 Gemma 3 모델들이 다양한 영역에서 일관된 성능 향상을 달성했음을 보여주며, 특히 수학, 코딩, 다국어 능력에서 두드러진 개선을 나타냅니다. - - - ### References -* [Gemma 3 Technical Report](http://arxiv.org/pdf/2503.19786v1) +* [Gemma 3 Technical Report](https://arxiv.org/pdf/2503.19786v1) diff --git a/_posts/2025-10-03-kimi-k2--open-agentic-intelligence.md b/_posts/2025-10-03-kimi-k2--open-agentic-intelligence.md index cb0077e..3958482 100644 --- a/_posts/2025-10-03-kimi-k2--open-agentic-intelligence.md +++ b/_posts/2025-10-03-kimi-k2--open-agentic-intelligence.md @@ -878,4 +878,4 @@ Muon에서는 가중치와 업데이트 모두 Adam보다 높은 효과적 순 H2D, 브로드캐스트, 가중치 재로드를 오버랩함으로써 훈련 엔진에서 모든 추론 엔진으로 가중치를 재샤딩하는 높은 대역폭을 얻을 수 있습니다. - - - ### References -* [Kimi K2: Open Agentic Intelligence](http://arxiv.org/pdf/2507.20534v1) +* [Kimi K2: Open Agentic Intelligence](https://arxiv.org/pdf/2507.20534v1) diff --git a/_posts/2025-10-05-deepseek-v2--a-strong--economical--and-efficient-mixture-of-experts-language-model.md b/_posts/2025-10-05-deepseek-v2--a-strong--economical--and-efficient-mixture-of-experts-language-model.md index 0acd287..30e29ad 100644 --- a/_posts/2025-10-05-deepseek-v2--a-strong--economical--and-efficient-mixture-of-experts-language-model.md +++ b/_posts/2025-10-05-deepseek-v2--a-strong--economical--and-efficient-mixture-of-experts-language-model.md @@ -32,7 +32,7 @@ MLA 구현에서는 128개의 어텐션 헤드와 128의 헤드당 차원을 설 ### 이 연구의 결과가 가지는 의미는 무엇입니까? -DeepSeek-V2의 성과는 대규모 언어 모델 분야에 중요한 이정표를 제시합니다. 단 21B개의 활성화된 파라미터로 오픈소스 모델 중 최고 수준의 성능을 달성했으며, 동시에 DeepSeek 67B 대비 42.5%의 훈련 비용 절감, 93.3%의 KV 캐시 감소, 5.76배의 생성 처리량 향상을 이루어냈습니다. AlpacaEval 2.0에서 38.9의 길이 제어 승률, MT-Bench에서 8.97의 점수를 기록하며 개방형 대화 벤치마크에서도 탁월한 성능을 보여주었습니다. +DeepSeek-V2의 성과는 대규모 언어 모델 분야에 중요한 이정표를 제시합니다. 단 21B개의 활성화된 파라미터로 오픈소스 모델 중 최고 수준의 성능을 달성했으며, 동시에 DeepSeek 67B 대비 42.5%의 훈련 비용 절감, 93.3%의 KV 캐시 감소, 5.76배의 생성 처리량 향상을 이루어냈습니다. DeepSeek-V2 Chat (RL)은 AlpacaEval 2.0에서 38.9의 길이 제어 승률, MT-Bench에서 8.97의 점수를 기록하며 개방형 대화 벤치마크에서도 탁월한 성능을 보여주었습니다. 이 연구는 단순한 기술적 혁신을 넘어 AI 기술의 실용적 배포와 접근성 향상에 중요한 기여를 합니다. MLA와 DeepSeekMoE 아키텍처는 대규모 언어 모델의 계산 효율성과 성능 사이의 트레이드오프를 근본적으로 재정의했으며, 오픈소스 AI 생태계의 발전에 새로운 방향을 제시했습니다. 특히 중국어와 영어를 동시에 지원하는 고성능 이중 언어 모델을 개발함으로써 글로벌 AI 연구에 의미 있는 진전을 이루었습니다. - - - @@ -795,4 +795,4 @@ $$\mathbf{u}_t = W^O[\mathbf{o}_{t,1}; \mathbf{o}_{t,2}; ...; \mathbf{o}_{t,n_h} 이러한 수식들은 MLA가 어떻게 효율적인 추론을 달성하면서도 강력한 성능을 유지하는지를 보여주는 핵심적인 수학적 기반을 제공합니다. - - - ### References -* [DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model](http://arxiv.org/pdf/2405.04434v5) +* [DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model](https://arxiv.org/pdf/2405.04434v5) diff --git a/_posts/2025-10-06-tulu-3--pushing-frontiers-in-open-language-model-post-training.md b/_posts/2025-10-06-tulu-3--pushing-frontiers-in-open-language-model-post-training.md index 27932f4..7712235 100644 --- a/_posts/2025-10-06-tulu-3--pushing-frontiers-in-open-language-model-post-training.md +++ b/_posts/2025-10-06-tulu-3--pushing-frontiers-in-open-language-model-post-training.md @@ -1190,4 +1190,4 @@ Tülu 3의 공개는 언어 모델 연구 분야에 장기적인 영향을 미 Tülu 3는 오픈소스 언어 모델 연구의 새로운 표준을 제시하며, 폐쇄형 모델과의 성능 격차를 해소하는 동시에 완전한 투명성을 달성한 중요한 이정표입니다. 이 프로젝트가 제공하는 자원들과 방법론들은 향후 언어 모델 연구의 발전에 지속적으로 기여할 것으로 기대됩니다. - - - #### References -* [Tulu 3: Pushing Frontiers in Open Language Model Post-Training](http://arxiv.org/pdf/2411.15124v5) +* [Tulu 3: Pushing Frontiers in Open Language Model Post-Training](https://arxiv.org/pdf/2411.15124v5) diff --git a/_posts/2025-10-07-deepseek-r1--incentivizing-reasoning-capability-in-llms-via-reinforcement-learning.md b/_posts/2025-10-07-deepseek-r1--incentivizing-reasoning-capability-in-llms-via-reinforcement-learning.md index 73823d2..7d8f0be 100644 --- a/_posts/2025-10-07-deepseek-r1--incentivizing-reasoning-capability-in-llms-via-reinforcement-learning.md +++ b/_posts/2025-10-07-deepseek-r1--incentivizing-reasoning-capability-in-llms-via-reinforcement-learning.md @@ -150,9 +150,9 @@ DeepSeek-R1-Zero의 훈련을 위해 연구팀은 기본 모델이 지정된 지 | 템플릿 구성 요소 | 내용 | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 시스템 프롬프트 | 사용자와 어시스턴트 간의 대화. 사용자가 질문하면 어시스턴트가 해결합니다. 어시스턴트는 먼저 마음속으로 추론 과정을 생각한 다음 사용자에게 답변을 제공합니다. 추론 과정과 답변은 각각 <think> </think>와 <answer> </answer> 태그로 둘러싸입니다. | +| 시스템 프롬프트 | 사용자와 어시스턴트 간의 대화. 사용자가 질문하면 어시스턴트가 해결합니다. 어시스턴트는 먼저 마음속으로 추론 과정을 생각한 다음 사용자에게 답변을 제공합니다. 추론 과정과 답변은 각각 `<think> </think>`와 `<answer> </answer>` 태그로 둘러싸입니다. | | 사용자 입력 | prompt | -| 어시스턴트 응답 | <think> 추론 과정 </think><answer> 답변 </answer> | +| 어시스턴트 응답 | `<think>` 추론 과정 `</think><answer>` 답변 `</answer>` | 이 템플릿은 의도적으로 구조적 형식에만 제약을 두고, 반성적 추론을 의무화하거나 특정 문제 해결 전략을 촉진하는 것과 같은 내용별 편향을 피했습니다. 이러한 접근법을 통해 연구팀은 강화학습 과정에서 모델의 자연스러운 진행 과정을 정확하게 관찰할 수 있었습니다. @@ -494,4 +494,4 @@ DeepSeek-R1 개발 초기 단계에서 연구팀은 여러 실패와 좌절을 연구팀이 공개한 DeepSeek-R1-Zero, DeepSeek-R1, 그리고 다양한 크기의 증류된 모델들은 연구 커뮤니티가 추론 능력 향상에 대한 연구를 더욱 발전시킬 수 있는 귀중한 자원이 될 것입니다. 이러한 오픈소스 접근법은 AI 연구의 민주화와 투명성을 촉진하며, 더 많은 연구자들이 고급 추론 모델 개발에 참여할 수 있는 기회를 제공합니다. - - - ### References -* [DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning](http://arxiv.org/pdf/2501.12948v1) +* [DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning](https://arxiv.org/pdf/2501.12948v1) diff --git a/_posts/2025-10-07-deepseek-v3-technical-report.md b/_posts/2025-10-07-deepseek-v3-technical-report.md index ef7ec0d..dd4cd8e 100644 --- a/_posts/2025-10-07-deepseek-v3-technical-report.md +++ b/_posts/2025-10-07-deepseek-v3-technical-report.md @@ -32,7 +32,7 @@ DeepSeek-V3의 구현은 매우 정교하고 세심한 엔지니어링 접근법 ### 이 연구의 결과가 가지는 의미는 무엇입니까? -DeepSeek-V3는 오픈소스 대규모 언어 모델 분야에서 획기적인 성과를 달성했습니다. 다양한 벤치마크에서 GPT-4o와 Claude-Sonnet 3.5와 같은 최첨단 클로즈드소스 모델들과 경쟁할 만한 성능을 보여주었으며, 특히 코드, 수학, 다국어 작업에서 뛰어난 능력을 입증했습니다. MMLU-Pro에서 75.9점, GPQA-Diamond에서 59.1점, MATH-500에서 90.2점 등 여러 벤치마크에서 최고 성능을 달성했습니다. +DeepSeek-V3는 오픈소스 대규모 언어 모델 분야에서 획기적인 성과를 달성했습니다. 다양한 벤치마크에서 GPT-4o와 Claude-Sonnet 3.5와 같은 최첨단 클로즈드소스 모델들과 경쟁할 만한 성능을 보여주었으며, 특히 코드, 수학, 다국어 작업에서 뛰어난 능력을 입증했습니다. MMLU-Pro 75.9점, GPQA-Diamond 59.1점으로 오픈소스 모델 중 가장 높았고, MATH-500에서는 90.2점으로 비공개 모델을 포함한 비교 대상 중 최고 점수를 기록했습니다. 더욱 주목할 만한 점은 이러한 뛰어난 성능을 매우 경제적인 비용으로 달성했다는 것입니다. 전체 훈련에 단지 2.788M H800 GPU 시간(약 $5.576M)만 소요되었으며, 이는 기존 대규모 모델 훈련과 비교했을 때 획기적으로 낮은 비용입니다. 이러한 성과는 오픈소스 AI 생태계의 민주화와 접근성 향상에 중요한 기여를 할 것으로 기대됩니다. - - - @@ -1032,4 +1032,4 @@ DeepSeek-V3의 FP8 혼합 정밀도 훈련 프레임워크의 효과를 검증 DeepSeek-V3에서 채택한 보조 손실 없는 전략은 이러한 딜레마를 해결하는 혁신적인 접근법입니다. 편향 항을 통한 동적 조정을 통해 로드 밸런스를 유지하면서도 전문가들의 자연스러운 전문화를 허용합니다. 이는 성능과 효율성을 모두 달성하는 균형잡힌 솔루션을 제공합니다. - - - ### References -* [DeepSeek-V3 Technical Report](http://arxiv.org/pdf/2412.19437v2) +* [DeepSeek-V3 Technical Report](https://arxiv.org/pdf/2412.19437v2) diff --git a/_posts/2025-10-07-gemma-2--improving-open-language-models-at-a-practical-size.md b/_posts/2025-10-07-gemma-2--improving-open-language-models-at-a-practical-size.md index bcd051b..4cfcaf9 100644 --- a/_posts/2025-10-07-gemma-2--improving-open-language-models-at-a-practical-size.md +++ b/_posts/2025-10-07-gemma-2--improving-open-language-models-at-a-practical-size.md @@ -14,7 +14,7 @@ use_math: true ## TL;DR ### 이 연구를 시작하게 된 배경과 동기는 무엇입니까? -대규모 언어 모델(LLM)의 발전은 최근 인공지능 분야에서 가장 주목받는 연구 영역 중 하나입니다. 기존의 대규모 모델들은 놀라운 성능을 보여주었지만, 대부분 계산 비용이 매우 높고 접근성이 제한적이었습니다. 특히 소규모 모델들의 성능 개선은 주로 훈련 길이 증가에 의존해 왔으며, 이는 데이터셋 크기에 대해 로그적으로만 확장되는 한계를 가지고 있었습니다. **Chinchilla 논문**에서 제시된 스케일링 법칙에 따르면, 최신 소형 모델들이 최첨단 성능을 1-2% 개선하기 위해서는 최대 15조 토큰이 필요하다는 점이 이러한 접근법의 비효율성을 잘 보여줍니다. +대규모 언어 모델(LLM)의 발전은 최근 인공지능 분야에서 가장 주목받는 연구 영역 중 하나입니다. 기존의 대규모 모델들은 놀라운 성능을 보여주었지만, 대부분 계산 비용이 매우 높고 접근성이 제한적이었습니다. 특히 소규모 모델들의 성능 개선은 주로 훈련 길이 증가에 의존해 왔으며, 이는 데이터셋 크기에 대해 로그적으로만 확장되는 한계를 가지고 있었습니다. Llama 3의 사례처럼 최신 소형 모델들이 최첨단 성능을 1-2% 미만 개선하는 데 최대 15조 토큰이 필요했다는 점이 이러한 접근법의 비효율성을 잘 보여줍니다. 이러한 배경에서 Google DeepMind의 Gemma 팀은 소규모 언어 모델의 성능을 획기적으로 개선할 수 있는 새로운 접근법을 모색하게 되었습니다. 그들의 핵심 동기는 단순히 모델 크기나 훈련 데이터의 양을 늘리는 대신, 훈련 과정에서 네트워크가 받는 정보의 질을 근본적으로 개선하는 것이었습니다. 특히 **지식 증류(knowledge distillation)** 기법을 활용하여 더 큰 교사 모델의 확률 분포를 작은 학생 모델이 학습하도록 함으로써, 모델의 성능을 획기적으로 향상시킬 수 있다는 가능성에 주목했습니다. @@ -34,7 +34,7 @@ Gemma 2는 20억 개에서 270억 개의 매개변수를 가진 세 가지 모 Gemma 2의 연구 결과는 소규모 언어 모델의 성능 향상에 대한 새로운 패러다임을 제시합니다. 지식 증류를 통해 2B 모델의 평균 성능을 처음부터 훈련한 모델 대비 7.4점 향상시켰으며, 심지어 2-3배 더 큰 모델들과도 경쟁력 있는 성능을 달성했습니다. 이는 단순히 모델 크기를 키우거나 훈련 데이터를 늘리는 대신, 훈련 과정에서 정보의 질을 개선하는 것이 더 효과적일 수 있음을 보여줍니다. -더불어 암송률과 개인 데이터 방출률에서도 획기적인 개선을 보였습니다. 암송률은 0.1% 미만, 개인 데이터 방출률은 0.00026%로 측정되어 프라이버시 보호 측면에서 중요한 진전을 나타냈습니다. 이러한 결과는 오픈 소스 언어 모델의 접근성을 높이면서도 안전성과 윤리적 고려사항을 동시에 해결할 수 있는 가능성을 보여줍니다. 특히 Responsible Generative AI Toolkit과 같은 추가 도구들을 통해 AI 기술의 긍정적인 사회적 영향을 확대할 수 있는 잠재력을 제시했습니다. +더불어 암송 평가에서도 낮은 수치를 보였습니다. 암송률은 0.1% 미만이었고, 암송된 출력 중 경미한 수준의 개인정보를 포함한 비율은 0.00026%에 그쳐 프라이버시 보호 측면에서 중요한 진전을 나타냈습니다. 이러한 결과는 오픈 소스 언어 모델의 접근성을 높이면서도 안전성과 윤리적 고려사항을 동시에 해결할 수 있는 가능성을 보여줍니다. 특히 Responsible Generative AI Toolkit과 같은 추가 도구들을 통해 AI 기술의 긍정적인 사회적 영향을 확대할 수 있는 잠재력을 제시했습니다. - - - ## 초록 @@ -46,7 +46,7 @@ Gemma 2의 가장 혁신적인 접근법 중 하나는 2B와 9B 모델에서 기 대규모 언어 모델(LLM)은 언어 이해, 생성, 추론에서 강력한 능력을 보여주었습니다. [GPT-3 논문](https://arxiv.org/pdf/2005.14165)에서 입증된 바와 같이, 스케일링이 이러한 최근 진전의 핵심이었으며, 많은 새로운 능력들이 오직 대규모에서만 나타났습니다. 최신 대형 모델들은 추론 벤치마크에서 전례 없는 성능에 도달할 뿐만 아니라, 멀티모달 및 다국어 능력, 그리고 100만 토큰 이상의 컨텍스트 길이를 처리하는 능력까지 보여줍니다. -소규모 모델들도 급속한 성능 향상을 보였지만, 이러한 개선은 주로 훈련 길이 증가에서 비롯되었습니다. 그러나 이 접근법은 데이터셋 크기에 대해 로그적으로만 확장되며, [Chinchilla 논문](https://arxiv.org/pdf/2203.15556)의 스케일링 법칙에 따르면 최신 소형 모델들은 최첨단을 1-2% 미만 개선하기 위해 최대 15T 토큰이 필요합니다. 그럼에도 불구하고 이러한 지속적인 개선은 소형 모델들이 여전히 과소훈련되어 있다는 증거를 제공합니다. +소규모 모델들도 급속한 성능 향상을 보였지만, 이러한 개선은 주로 훈련 길이 증가에서 비롯되었습니다. 그러나 이 접근법은 데이터셋 크기에 대해 로그적으로만 확장되며, Llama 3의 사례처럼 최신 소형 모델들은 최첨단을 1-2% 미만 개선하는 데 최대 15T 토큰이 필요했습니다. 그럼에도 불구하고 이러한 지속적인 개선은 소형 모델들이 여전히 과소훈련되어 있다는 증거를 제공합니다. ### 지식 증류를 통한 혁신적 접근 @@ -596,7 +596,7 @@ Gemma 2는 이러한 질문들에서 참가자들을 잘못된 답으로 설득 ### 책임감 있는 배포와 안전성 -이러한 성격의 되돌릴 수 없는 공개에는 내재적 위험이 있지만, 앞서 설명한 광범위한 안전 조사와 책임감 있는 배포 절차를 통해 이러한 모델들이 커뮤니티에 순긍정적인 영향을 미칠 것이라는 확신을 갖고 있습니다. 특히 암송률이 0.1% 미만으로 나타난 결과와 개인 데이터 방출률이 0.00026%로 매우 낮게 측정된 것은 프라이버시 보호 측면에서 중요한 진전을 나타냅니다. +이러한 성격의 되돌릴 수 없는 공개에는 내재적 위험이 있지만, 앞서 설명한 광범위한 안전 조사와 책임감 있는 배포 절차를 통해 이러한 모델들이 커뮤니티에 순긍정적인 영향을 미칠 것이라는 확신을 갖고 있습니다. 특히 암송률이 0.1% 미만이고, 암송된 출력 중 경미한 개인정보를 포함한 비율이 0.00026%에 그친 것은 프라이버시 보호 측면에서 중요한 진전을 나타냅니다. ### 모델의 한계와 향후 연구 방향 @@ -621,4 +621,4 @@ Gemma 2는 이러한 질문들에서 참가자들을 잘못된 답으로 설득 Gemma 2는 지식 증류의 효과성을 입증하고, 출력 확률에 대한 훈련이 다음 토큰 예측 대비 제공하는 이점을 보여주는 중요한 기여를 했습니다. 이러한 혁신들은 소형 모델의 성능을 크게 향상시키면서도 계산 효율성을 유지할 수 있는 새로운 패러다임을 제시했습니다. 모델의 한계를 인정하면서도, 책임감 있는 개발과 배포를 통해 AI 기술의 이익을 사회 전반에 확산시킬 수 있는 가능성을 보여주었습니다. - - - ### References -* [Gemma 2: Improving Open Language Models at a Practical Size](http://arxiv.org/pdf/2408.00118v3) +* [Gemma 2: Improving Open Language Models at a Practical Size](https://arxiv.org/pdf/2408.00118v3) diff --git a/_posts/2025-10-07-the-llama-3-herd-of-models.md b/_posts/2025-10-07-the-llama-3-herd-of-models.md index aefc0ec..ec6d8c4 100644 --- a/_posts/2025-10-07-the-llama-3-herd-of-models.md +++ b/_posts/2025-10-07-the-llama-3-herd-of-models.md @@ -1572,4 +1572,4 @@ Llama 3의 개발 경험은 파운데이션 모델 분야에서 여전히 상당 Llama 3의 성공적인 개발과 공개는 오픈 소스 AI 모델의 가능성을 보여주는 중요한 이정표가 되었으며, 향후 더욱 강력하고 안전한 AI 시스템 개발을 위한 기반을 마련했습니다. 연구진의 경험과 통찰은 AI 연구의 미래 방향을 제시하는 귀중한 자료가 될 것입니다. - - - #### References -* [The Llama 3 Herd of Models](http://arxiv.org/pdf/2407.21783v3) +* [The Llama 3 Herd of Models](https://arxiv.org/pdf/2407.21783v3) diff --git a/_posts/2025-10-08-qwen3-technical-report.md b/_posts/2025-10-08-qwen3-technical-report.md index b038c11..242fd77 100644 --- a/_posts/2025-10-08-qwen3-technical-report.md +++ b/_posts/2025-10-08-qwen3-technical-report.md @@ -32,7 +32,7 @@ Qwen3의 구현은 3단계 사전 훈련 전략을 중심으로 이루어졌습 ### 이 연구의 결과가 가지는 의미는 무엇입니까? -Qwen3의 실험 결과는 매우 인상적입니다. Qwen3-235B-A22B 모델은 AIME'24에서 85.7점, LiveCodeBench v5에서 70.7점 등 다양한 벤치마크에서 최첨단 성능을 달성했습니다. 특히 주목할 만한 점은 DeepSeek-V3와 같은 기존 최고 모델들과 비교하여 총 매개변수의 1/3, 활성화 매개변수의 2/3만으로 대부분의 벤치마크에서 우수한 성능을 보였다는 것입니다. +Qwen3의 실험 결과는 매우 인상적입니다. Qwen3-235B-A22B 모델은 AIME'24에서 85.7점, LiveCodeBench v5에서 70.7점 등 다양한 벤치마크에서 최첨단 성능을 달성했습니다. 기반 모델 비교에서도 Qwen3-235B-A22B-Base는 DeepSeek-V3-Base 대비 총 매개변수 약 1/3, 활성 매개변수 2/3로 15개 벤치마크 중 14개에서 앞섰습니다. 이 연구는 대규모 언어 모델 분야에 여러 중요한 시사점을 제공합니다. 첫째, 모델의 추론 능력을 동적으로 제어할 수 있는 새로운 접근법을 제시했으며, 둘째 오픈소스 모델의 성능과 효율성에 대한 새로운 기준을 제시했습니다. 특히 119개 언어를 지원하며 Apache 2.0 라이선스로 공개됨으로써 AI 연구의 개방성과 접근성을 크게 향상시켰다는 점에서 그 의의가 큽니다. - - - @@ -408,4 +408,4 @@ SFT 데이터셋은 "사고"와 "비사고" 데이터를 모두 결합합니다. 위 표는 사고 모드 융합 단계에서 사고 모드와 비사고 모드에 대한 SFT 데이터의 예시를 보여줍니다. 사고 모드의 경우 기본 동작을 나타내므로 `/think` 플래그를 생략할 수 있습니다. - - - ### References -* [Qwen3 Technical Report](http://arxiv.org/pdf/2505.09388v1) +* [Qwen3 Technical Report](https://arxiv.org/pdf/2505.09388v1) diff --git a/_posts/2025-10-10-pixtral-12b.md b/_posts/2025-10-10-pixtral-12b.md index 770bd87..08cde39 100644 --- a/_posts/2025-10-10-pixtral-12b.md +++ b/_posts/2025-10-10-pixtral-12b.md @@ -470,4 +470,4 @@ Llama 전용 프롬프트를 사용했을 때 특히 11B 변형에서 MathVista 이러한 상세한 분석은 멀티모달 언어 모델 평가에서 프롬프트 설계와 평가 메트릭의 중요성을 강조하며, 공정하고 일관된 비교를 위한 표준화된 프로토콜의 필요성을 보여줍니다. - - - ### References -* [Pixtral 12B](http://arxiv.org/pdf/2410.07073v2) +* [Pixtral 12B](https://arxiv.org/pdf/2410.07073v2) diff --git a/_posts/2025-10-11-gemini--a-family-of-highly-capable-multimodal-models.md b/_posts/2025-10-11-gemini--a-family-of-highly-capable-multimodal-models.md index c1fb132..1c05876 100644 --- a/_posts/2025-10-11-gemini--a-family-of-highly-capable-multimodal-models.md +++ b/_posts/2025-10-11-gemini--a-family-of-highly-capable-multimodal-models.md @@ -1502,4 +1502,4 @@ plt.show() 이러한 정성적 예시들은 Gemini Ultra의 멀티모달 추론 능력과 다양한 도메인에서의 실제 적용 가능성을 보여주는 포괄적인 데모를 제공합니다. 각 예시는 모델이 텍스트, 이미지, 비디오 등 다양한 입력 모달리티를 자연스럽게 처리하고 의미 있는 응답을 생성할 수 있는 능력을 입증합니다. - - - ### References -* [Gemini: A Family of Highly Capable Multimodal Models](http://arxiv.org/pdf/2312.11805v5) +* [Gemini: A Family of Highly Capable Multimodal Models](https://arxiv.org/pdf/2312.11805v5) diff --git a/_posts/2025-10-11-gemini-2.5--pushing-the-frontier-with-advanced-reasoning--multimodality--long-context--and-next-generation-agentic-capabilities.md b/_posts/2025-10-11-gemini-2.5--pushing-the-frontier-with-advanced-reasoning--multimodality--long-context--and-next-generation-agentic-capabilities.md index a6c9fed..5298e18 100644 --- a/_posts/2025-10-11-gemini-2.5--pushing-the-frontier-with-advanced-reasoning--multimodality--long-context--and-next-generation-agentic-capabilities.md +++ b/_posts/2025-10-11-gemini-2.5--pushing-the-frontier-with-advanced-reasoning--multimodality--long-context--and-next-generation-agentic-capabilities.md @@ -6,7 +6,7 @@ subtitle: "Gemini 2.5: 첨단 추론, 멀티모달리티, 긴 컨텍스트, 차 date: 2025-07-07 17:36:04 paper_author: "Google DeepMind" description: >- - Gemini 2.5는 희소 MoE 트랜스포머에 사고 기능을 더해 최대 100만 토큰 컨텍스트와 3시간 분량 비디오를 처리하며, LiveCodeBench 74.2%, GPQA diamond 86.4%를 기록했습니다. 모델 구성과 훈련, 안전성 평가를 정리합니다. + Gemini 2.5는 희소 MoE 트랜스포머에 사고 기능을 더해 최대 100만 토큰 컨텍스트를 지원하고, 2.5 Pro는 3시간 분량 비디오를 처리하며 LiveCodeBench 74.2%, GPQA diamond 86.4%를 기록했습니다. 모델 구성과 훈련, 안전성 평가를 정리합니다. categories: ["Paper Reviews", "Multimodal-Learning"] tags: ["Mixture-of-Experts", "Long-Context-Reasoning", "Thinking-Budget", "Multimodal-Tool-Use", "Natively-Multimodal-Transformer", "Reinforcement-Learning-from-Human-and-Critic-Feedback", "Agentic-Workflow-Generation", "Thinking-Mode-Fusion", "Advanced-Reasoning-Capabilities", "Red-Teaming", "Reasoning-Models", "Multimodal-Models"] cover: /assets/images/multimodal-learning.jpg @@ -29,7 +29,7 @@ Gemini 2.5 모델 패밀리는 희소 전문가 혼합(Sparse Mixture-of-Experts Gemini 2.5 모델의 구현은 대규모 TPUv5p 인프라를 활용한 복잡한 훈련 과정을 통해 이루어졌습니다. 훈련 데이터셋은 웹 문서, 코드, 이미지, 오디오, 비디오 등 다양한 소스를 포함하며, 엄격한 데이터 품질 관리와 필터링 과정을 거쳤습니다. 특히 데이터 중복 제거와 안전성 필터링에 중점을 두어 모델의 성능과 신뢰성을 높였습니다. -훈련 과정에서는 지도 미세 조정(Supervised Fine-Tuning), 보상 모델링, 강화 학습 등 다양한 기법을 적용했습니다. 특히 Constitutional AI 접근법을 통해 모델이 자신의 응답을 비판하고 개선할 수 있도록 훈련했으며, 인간 피드백과 모델 기반 평가를 결합하여 모델의 안전성과 유용성을 개선했습니다. 또한 자동화된 레드팀 테스팅을 통해 잠재적 위험과 편향을 지속적으로 평가하고 완화했습니다. +훈련 과정에서는 지도 미세 조정(Supervised Fine-Tuning), 보상 모델링, 강화 학습 등 다양한 기법을 적용했습니다. SFT 데이터는 Constitutional AI에서 착안한 데이터 생성 방식과 사람의 응답 수정을 조합해 만들었고, 강화 학습 단계에서는 보상 모델과 루브릭으로 채점하는 Critic 모델을 함께 보상으로 쓰는 등 인간 피드백과 모델 기반 평가를 결합하여 모델의 안전성과 유용성을 개선했습니다. 또한 자동화된 레드팀 테스팅을 통해 잠재적 위험과 편향을 지속적으로 평가하고 완화했습니다. ### 이 연구의 결과가 가지는 의미는 무엇입니까? @@ -1027,4 +1027,4 @@ Gemini 1.5 Pro는 3건 중 1건에서 색깔을 맞추고, 3건 중 0건에서 이러한 결과는 Gemini 2.5 Pro가 긴 비디오 콘텐츠에서 특정 시각적 이벤트를 정확하게 회상하는 데 있어 이전 버전보다 현저히 향상된 능력을 보여줍니다. - - - #### References -* [Gemini 2.5: Pushing the Frontier with Advanced Reasoning, Multimodality, Long Context, and Next Generation Agentic Capabilities](http://arxiv.org/pdf/2507.06261v4) +* [Gemini 2.5: Pushing the Frontier with Advanced Reasoning, Multimodality, Long Context, and Next Generation Agentic Capabilities](https://arxiv.org/pdf/2507.06261v4) diff --git a/_posts/2025-10-19-direct-preference-optimization--your-language-model-is-secretly-a-reward-model.md b/_posts/2025-10-19-direct-preference-optimization--your-language-model-is-secretly-a-reward-model.md index a4d372d..76ff9e5 100644 --- a/_posts/2025-10-19-direct-preference-optimization--your-language-model-is-secretly-a-reward-model.md +++ b/_posts/2025-10-19-direct-preference-optimization--your-language-model-is-secretly-a-reward-model.md @@ -765,4 +765,4 @@ DPO 대 PPO-0 비교의 150개 무작위 샘플과 PPO-1 대 PPO-0 비교의 100 이러한 결과는 GPT-4가 인간 평가의 신뢰할 수 있는 대리 지표로 사용될 수 있음을 검증하지만, 프롬프트 설계가 평가 결과에 영향을 미칠 수 있음을 보여줍니다. Concise 프롬프트가 인간 판단과 더 잘 일치하는 경향이 있어, 이후 분석에서는 이 프롬프트를 사용했습니다. - - - ### References -* [Direct Preference Optimization: Your Language Model is Secretly a Reward Model](http://arxiv.org/pdf/2305.18290v3) +* [Direct Preference Optimization: Your Language Model is Secretly a Reward Model](https://arxiv.org/pdf/2305.18290v3) diff --git a/_posts/2025-10-24-qlora--efficient-finetuning-of-quantized-llms.md b/_posts/2025-10-24-qlora--efficient-finetuning-of-quantized-llms.md index 82908b7..3fc20ad 100644 --- a/_posts/2025-10-24-qlora--efficient-finetuning-of-quantized-llms.md +++ b/_posts/2025-10-24-qlora--efficient-finetuning-of-quantized-llms.md @@ -631,4 +631,4 @@ QLoRA는 사용자가 자신의 데이터와 모델을 소유하고 관리할 전반적으로 QLoRA가 고품질 LLM의 파인튜닝을 훨씬 더 광범위하고 쉽게 접근할 수 있게 만들어 광범위하게 긍정적인 영향을 미칠 것이라고 믿습니다. 이는 AI 기술의 민주화라는 더 큰 목표에 기여하며, 더 많은 사람들이 최신 언어 모델 기술을 활용하여 혁신적인 응용을 개발할 수 있게 합니다. 동시에 이러한 기술의 책임 있는 사용을 보장하기 위한 지속적인 연구와 커뮤니티의 노력이 필요합니다. - - - ### References -* [QLoRA: Efficient Finetuning of Quantized LLMs](http://arxiv.org/pdf/2305.14314v1) +* [QLoRA: Efficient Finetuning of Quantized LLMs](https://arxiv.org/pdf/2305.14314v1) diff --git a/_posts/2026-03-25-lightrag--simple-and-fast-retrieval-augmented-generation.md b/_posts/2026-03-25-lightrag--simple-and-fast-retrieval-augmented-generation.md index 44b8d46..39f2346 100644 --- a/_posts/2026-03-25-lightrag--simple-and-fast-retrieval-augmented-generation.md +++ b/_posts/2026-03-25-lightrag--simple-and-fast-retrieval-augmented-generation.md @@ -4,7 +4,7 @@ title: "LightRAG: Simple and Fast Retrieval-Augmented Generation" date: 2024-10-08 08:00:12 paper_author: "Beijing University of Posts and Telecommunications" description: >- - LightRAG는 그래프 기반 텍스트 인덱싱과 이중 수준 검색을 결합하고 증분 업데이트로 인덱스 재구축을 없앴습니다. 검색 단계에서 GraphRAG가 약 610,000 토큰을 쓰는 반면 100 토큰 미만만 씁니다. 인덱싱과 검색 구조, 비용 비교를 정리합니다. + LightRAG는 그래프 기반 텍스트 인덱싱과 이중 수준 검색을 결합하고 증분 업데이트로 인덱스 재구축을 없앴습니다. 검색 단계에서 GraphRAG는 약 610,000 토큰을 쓰지만 LightRAG는 100 토큰 미만만 씁니다. 인덱싱과 검색 구조, 비용 비교를 정리합니다. categories: ["Paper Reviews", "Retrieval-Augmented-Generation"] tags: ["Graph-Based-Text-Indexing", "Dual-Level-Retrieval-Paradigm", "Low-Level-Entity-Retrieval", "High-Level-Relationship-Retrieval", "Entity-and-Relationship-Extraction", "LLM-Profiling-for-Key-Value-Pair-Generation", "Incremental-Knowledge-Base-Updates", "Hybrid-Retrieval", "Multi-Hop-Subgraph-Information-Extraction", "Deduplication-for-Graph-Optimization", "Retrieval-Augmented-Generation", "Knowledge-Graph"] cover: /assets/images/retrieval-augmented-generation.jpg @@ -35,7 +35,7 @@ LightRAG의 구현은 세 가지 주요 단계로 구성됩니다. 첫 번째는 실험 결과는 LightRAG의 우월성을 명확하게 입증합니다. Legal 데이터셋에서 LightRAG는 Naive RAG 대비 Overall 기준으로 84.8% 승률을 기록했으며, RQ-RAG 대비 85.6%, HyDE 대비 73.6%의 승률을 달성했습니다. 특히 Diversity 차원에서 LightRAG는 Naive RAG 대비 86.4%, RQ-RAG 대비 88.4%의 압도적 우위를 보였습니다. 동일한 그래프 기반 검색을 활용하는 GraphRAG와의 비교에서도 LightRAG는 Agriculture, CS, Legal 데이터셋에서 각각 54.8%, 52.0%, 52.8%의 Overall 승률을 기록하며 일관된 우위를 확보했습니다. 어블레이션 연구 결과는 이중 수준 검색의 각 구성 요소가 성능에 기여함을 보여주었으며, 특히 고수준 검색 제거 시 Legal 데이터셋의 Overall이 84.8%에서 78.0%로 하락하고, 저수준 검색 제거 시 81.2%로 하락했습니다. -비용 효율성 측면에서 LightRAG는 GraphRAG 대비 극적인 개선을 달성했습니다. 검색 단계에서 GraphRAG는 Legal 데이터셋의 610개 커뮤니티 보고서를 순회하기 위해 약 610,000 토큰을 소비하고 610회의 API 호출이 필요한 반면, LightRAG는 100 토큰 미만만 소비하고 단 1회의 API 호출만 필요합니다. 이는 토큰 소비 기준으로 약 6,000배 이상의 효율성 차이에 해당합니다. 증분 데이터 업데이트에서도 GraphRAG는 새로운 데이터 추가 시 전체 커뮤니티 보고서를 재생성해야 하므로 약 13,990,000 토큰의 추가 비용이 발생하는 반면, LightRAG는 새 데이터의 추출 비용만 발생하여 사실상 추가 오버헤드가 없습니다. 이러한 결과는 LightRAG가 응답 품질에서의 우위와 함께 운용 비용과 동적 적응성 측면에서도 실질적 이점을 제공하는 프레임워크임을 명확히 보여줍니다. +비용 효율성 측면에서 LightRAG는 GraphRAG 대비 극적인 개선을 달성했습니다. 검색 단계에서 GraphRAG는 Legal 데이터셋의 610개 커뮤니티 보고서를 순회하기 위해 약 610,000 토큰을 소비하고 수백 회의 API 호출이 필요한 반면, LightRAG는 100 토큰 미만만 소비하고 단 1회의 API 호출만 필요합니다. 이는 토큰 소비 기준으로 약 6,000배 이상의 효율성 차이에 해당합니다. 증분 데이터 업데이트에서도 GraphRAG는 새로운 데이터 추가 시 전체 커뮤니티 보고서를 재생성해야 하므로 약 13,990,000 토큰의 추가 비용이 발생하는 반면, LightRAG는 새 데이터의 추출 비용만 발생하여 사실상 추가 오버헤드가 없습니다. 이러한 결과는 LightRAG가 응답 품질에서의 우위와 함께 운용 비용과 동적 적응성 측면에서도 실질적 이점을 제공하는 프레임워크임을 명확히 보여줍니다. 본 연구의 의의는 대규모 동적 환경에서의 RAG 시스템 운용 가능성을 한 단계 끌어올렸다는 점에 있습니다. GraphRAG가 그래프 모듈러리티를 통한 계층적 요약이라는 기초적 접근법을 확립했다면, LightRAG는 키-값 기반 직접 검색과 증분 업데이트라는 실용적 효율성을 추가하여 실제 운영 환경에서의 확장성을 크게 향상시켰습니다. 향후 연구 방향으로는 더욱 정교한 엔터티 중복 제거 기법, 다국어 지원 확대, 그리고 도메인 특화 엔터티 타입의 자동 학습 등이 고려될 수 있습니다. - - - @@ -447,7 +447,7 @@ LightRAG의 실용적 효율성을 검증하기 위해 가장 경쟁력 있는 | 단계 | 지표 | GraphRAG | LightRAG | |---|---|---|---| | **검색 단계** | 토큰 | $$610 \times 1{,}000$$ | $$< 100$$ | -| | API 호출 | $$610$$ | $$1$$ | +| | API 호출 | $$610 \times 1{,}000 / C_{\text{max}}$$ | $$1$$ | | **증분 업데이트** | 토큰 | $$1{,}399 \times 2 \times 5{,}000 + T_{\text{extract}}$$ | $$T_{\text{extract}}$$ | | | API 호출 | $$1{,}399 \times 2 + C_{\text{extract}}$$ | $$C_{\text{extract}}$$ | diff --git a/_posts/2026-03-31-claude-code-source-map-leak-analysis-en.md b/_posts/2026-03-31-claude-code-source-map-leak-analysis-en.md index aed0bfb..36d0b1b 100644 --- a/_posts/2026-03-31-claude-code-source-map-leak-analysis-en.md +++ b/_posts/2026-03-31-claude-code-source-map-leak-analysis-en.md @@ -5,7 +5,7 @@ subtitle: "Claude Code Exposed: Anatomy of an Agentic AI Through an npm Source M description: >- An npm source map leak exposed all 4,600+ files of Claude Code's core engine. Its 8-layer security and 4-tier message compaction, read as architecture. -date: 2026-03-31 12:00:01 +date: 2026-03-31 11:59:59 categories: ["Insights", "Agentic-AI"] tags: ["Claude-Code", "Agentic-Architecture", "Context-Compaction", "Multi-Agent-Orchestration", "Security-Architecture", "Agentic-AI"] cover: /assets/images/insights.jpg diff --git a/_posts/2026-04-05-evolution-of-ai-agentic-patterns-en.md b/_posts/2026-04-05-evolution-of-ai-agentic-patterns-en.md index 0bf0b89..c4b80c0 100644 --- a/_posts/2026-04-05-evolution-of-ai-agentic-patterns-en.md +++ b/_posts/2026-04-05-evolution-of-ai-agentic-patterns-en.md @@ -5,7 +5,7 @@ subtitle: "Engineering Rigor Doesn't Disappear — It Relocates. Four Years of A description: >- Engineering rigor never disappeared — it relocated. Four years, three paradigm shifts from prompts to context to harnesses, traced by why each era failed. -date: 2026-04-05 12:00:00 +date: 2026-04-05 11:59:59 categories: ["Insights", "Agentic-AI"] tags: ["Prompt-Engineering", "Context-Engineering", "Harness-Engineering", "Agentic-Patterns", "LLM-Architecture", "Vibe-Coding", "Agentic-AI"] cover: /assets/images/insights.jpg @@ -42,7 +42,7 @@ This post has one thesis: **Engineering rigor never disappeared. It relocated.** Each relocation was triggered by the previous era's failure. Let's trace that arc. -<a href="/assets/images/three-era-timeline.svg" class="glightbox" data-gallery="agentic-patterns" data-glightbox="title: AI 에이전틱 패턴의 진화 타임라인"> +<a href="/assets/images/three-era-timeline.svg" class="glightbox" data-gallery="agentic-patterns" data-glightbox="title: Evolution timeline of AI agentic patterns"> <img src="/assets/images/three-era-timeline.svg" alt="Evolution timeline of AI agentic patterns — Prompt → Context → Harness Engineering" /> </a> @@ -285,7 +285,7 @@ The metric they called "the single most important metric" for production agents Here's how KV-cache works. When you send a prompt to an LLM API, the model computes attention Keys and Values for each token. This computation is expensive. But if the **prefix** of the current request matches the previous request, that portion can reuse cached results — no recomputation needed. On Claude Sonnet, a cache hit reduces cost by **10x**. The difference between an agent re-computing the system prompt across 30 turns versus leveraging the cache is enormous. -<a href="/assets/images/kv-cache-mechanism.svg" class="glightbox" data-gallery="agentic-patterns" data-glightbox="title: KV-cache 작동 원리 — 안정 접두어가 비용을 결정한다"> +<a href="/assets/images/kv-cache-mechanism.svg" class="glightbox" data-gallery="agentic-patterns" data-glightbox="title: How KV-cache works — the stable prefix determines cost"> <img src="/assets/images/kv-cache-mechanism.svg" alt="How KV-cache works — the stable prefix determines cost" /> </a> diff --git a/_posts/2026-04-12-agentcore-harness-engineering-analysis.md b/_posts/2026-04-12-agentcore-harness-engineering-analysis.md index 8b08197..600ab50 100644 --- a/_posts/2026-04-12-agentcore-harness-engineering-analysis.md +++ b/_posts/2026-04-12-agentcore-harness-engineering-analysis.md @@ -117,7 +117,7 @@ response1 = agent_core_client.InvokeAgentRuntime( Runtime은 두 개의 HTTP 엔드포인트로 외부 요청을 받습니다. `/invocations`는 에이전트 호출을 받고 JSON 응답과 **SSE**(Server-Sent Events — 서버가 한 HTTP 연결을 열어 두고 클라이언트에 이벤트를 한 방향으로 연속 스트리밍하는 표준, ChatGPT식 타이핑 효과가 이 방식) 스트리밍 모두 지원합니다. `/ping`은 헬스체크인데 단순히 `ok`를 돌려주지 않고 **`Healthy` 또는 `HealthyBusy` 두 상태**를 반환합니다. `Healthy`는 새 작업을 받을 준비가 되었다는 신호이고, `HealthyBusy`는 살아 있지만 비동기 태스크 처리 중이므로 새 호출을 받지 않겠다는 신호입니다. 후자로 바뀐 동안 로드밸런서나 프론트 프록시가 이 세션으로 새 요청을 보내지 않도록 막히므로, 긴 리서치 작업이 백그라운드에서 도는 도중 동일 세션에 중복 요청이 들어와 상태가 꼬이는 사고가 원천 차단됩니다. -<a href="https://miro.medium.com/v2/resize:fit:720/format:webp/1*n4jZh_ngE87Updy4bUjHXg.png" class="glightbox" data-gallery="agentcore" data-glightbox="title: `/ping` 엔드포인트의 두 상태 — 평상시 HEALTHY, 비동기 태스크 실행 중에는 HEALTHY_BUSY로 전환 (출처: Joud W. Awad, AWS Bedrock AgentCore Deep Dive)"> +<a href="https://miro.medium.com/v2/resize:fit:720/format:webp/1*n4jZh_ngE87Updy4bUjHXg.png" class="glightbox" data-gallery="agentcore" data-glightbox="title: /ping 엔드포인트의 두 상태 — 평상시 HEALTHY, 비동기 태스크 실행 중에는 HEALTHY_BUSY로 전환 (출처: Joud W. Awad, AWS Bedrock AgentCore Deep Dive)"> <img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*n4jZh_ngE87Updy4bUjHXg.png" alt="/ping 엔드포인트가 반환하는 두 가지 상태. 위쪽은 평상시 HEALTHY, 아래쪽은 사용자가 백그라운드 태스크를 시작한 직후 HEALTHY_BUSY로 전환되는 상황." /> </a> diff --git a/_posts/2026-06-07-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md b/_posts/2026-06-07-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md index 77eaa3d..08e6eed 100644 --- a/_posts/2026-06-07-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md +++ b/_posts/2026-06-07-zep-a-temporal-knowledge-graph-architecture-for-agent-memory.md @@ -519,7 +519,7 @@ LongMemEval은 [Wu 등](https://arxiv.org/pdf/2410.10813v2)이 제안한 벤치 #### LongMemEval 결과 -Zep은 두 모델 변형 모두에서 정확도와 지연 시간 양쪽에 걸쳐 상당한 개선을 보였습니다. gpt-4o-mini에서는 베이스라인 대비 정확도가 15.2%(55.4%에서 63.8%) 향상되었고, gpt-4o에서는 18.5%(60.2%에서 71.2%) 향상되었습니다. 특히 주목할 부분은 검색을 통해 프롬프트 크기를 극적으로 줄임으로써 지연 시간까지 대폭 절감했다는 점입니다. 다음 표가 이를 종합적으로 보여줍니다. +Zep은 두 모델 변형 모두에서 정확도와 지연 시간 양쪽에 걸쳐 상당한 개선을 보였습니다. gpt-4o-mini에서는 베이스라인 대비 정확도가 15.2%(55.4%에서 63.8%) 향상되었고, gpt-4o에서는 60.2%에서 71.2%로 약 18% 향상되었습니다(논문 본문 표기는 18.5%). 특히 주목할 부분은 검색을 통해 프롬프트 크기를 극적으로 줄임으로써 지연 시간까지 대폭 절감했다는 점입니다. 다음 표가 이를 종합적으로 보여줍니다. | 메모리 방식 | 모델 | 점수 | 지연 시간 | 지연 IQR | 평균 맥락 토큰 | |---|---|---|---|---|---| diff --git a/_posts/2026-07-27-ai-ready-data-semantic-layer-knowledge-graph-en.md b/_posts/2026-07-27-ai-ready-data-semantic-layer-knowledge-graph-en.md index f9c1eae..b04aec2 100644 --- a/_posts/2026-07-27-ai-ready-data-semantic-layer-knowledge-graph-en.md +++ b/_posts/2026-07-27-ai-ready-data-semantic-layer-knowledge-graph-en.md @@ -4,7 +4,7 @@ title: "Writing Context Into Your Data — AI-Ready Data, Semantic Layers, Knowl description: >- "AI-ready data" isn't clean data — it's context written in machine-executable form. Why the same schema takes text-to-SQL accuracy from 16.7% to 54.2%. -date: 2026-07-27 12:00:00 +date: 2026-07-27 11:59:59 categories: ["Insights", "Data-Architecture"] tags: ["AI-Ready-Data", "Semantic-Layer", "Knowledge-Graph", "Ontology", "GraphRAG", "Agentic-AI", "Model-Context-Protocol", "Data-Governance"] cover: /assets/images/insights.jpg diff --git a/_sass/_dark.scss b/_sass/_dark.scss index 400271e..364daa6 100644 --- a/_sass/_dark.scss +++ b/_sass/_dark.scss @@ -155,7 +155,7 @@ &:focus { border-color: $link; box-shadow: 0 0 0 3px rgba($link, 0.25); } } .search-result { border-bottom-color: $border; } - .search-result h3 a { color: $fg; &:hover { color: $link; } } + .search-result h2 a { color: $fg; &:hover { color: $link; } } // .result-tags was missing here, so it kept search.css's #767676 — a value // chosen to clear 4.5:1 on white, which lands at 4.2:1 on #0d1117 and fails AA // for text this size. diff --git a/_sass/base/_grid-settings.scss b/_sass/base/_grid-settings.scss index a5a3ffb..f743086 100644 --- a/_sass/base/_grid-settings.scss +++ b/_sass/base/_grid-settings.scss @@ -4,5 +4,3 @@ $medium-screen: em(640); $large-screen: em(860); -$medium-screen-up: new-breakpoint(min-width $medium-screen 4); -$large-screen-up: new-breakpoint(min-width $large-screen 8); diff --git a/_sass/base/_lists.scss b/_sass/base/_lists.scss index 5048860..7dde0aa 100644 --- a/_sass/base/_lists.scss +++ b/_sass/base/_lists.scss @@ -1,7 +1,6 @@ ul, ol { - &%default-ul, - &%default-ol { + &%default-ul { list-style-type: none; margin: 0; padding: 0; diff --git a/_sass/base/_mixins.scss b/_sass/base/_mixins.scss index 2582c2e..0f72eb1 100644 --- a/_sass/base/_mixins.scss +++ b/_sass/base/_mixins.scss @@ -7,35 +7,3 @@ transform: scale($scale); } } - -// Hover Transform with Shadow -@mixin hover-transform-shadow($scale: 1.05, $shadow: $shadow-medium) { - transition: $transition-transform, box-shadow 0.3s ease; - cursor: pointer; - will-change: transform, box-shadow; - - &:hover { - transform: scale($scale); - box-shadow: $shadow; - } -} - -// Smooth Color Transition -@mixin hover-color($color: $action-color, $transition: $transition-color) { - transition: $transition; - - &:hover { - color: $color; - } -} - -// Button Hover Effect -@mixin button-hover($bg-color: $action-color, $text-color: $white) { - transition: background-color 0.3s ease, color 0.3s ease; - - &:hover, - &:focus { - background-color: $bg-color; - color: $text-color; - } -} \ No newline at end of file diff --git a/_sass/base/_variables.scss b/_sass/base/_variables.scss index ef8c122..ea8918c 100644 --- a/_sass/base/_variables.scss +++ b/_sass/base/_variables.scss @@ -21,7 +21,6 @@ $heading-line-height: 1.3; // was 1.6 — loose at display sizes, where a 48px t $base-border-radius: 4px; $base-spacing: $base-line-height * $em-base; $small-spacing: $base-spacing / 2; -$base-z-index: 0; // Colors $dark-gray: #333; @@ -30,10 +29,8 @@ $light-gray: #ddd; $white: #fff; // Brand colors. The blog's identity is space exploration (astronaut logo, James // Webb nebula cover), so the accent is a deeper "cosmic blue" than the old -// generic #2980b9, with a cosmic-purple secondary for hover/highlight accents. +// generic #2980b9. $blue: #1c5d99; // Cosmic blue — primary accent (link-on-white 6.83:1) -$purple: #6a4c93; // Cosmic purple — secondary accent (6.85:1) -$red: #c0392b; // Pomegranate — error/warning accents // Theme Colors $base-background-color: $white; @@ -55,8 +52,6 @@ $transition-color: color 0.3s ease; $transition-all: all 0.3s ease; // Shadows -$shadow-light: 0 2px 4px rgba($dark-gray, 0.1); -$shadow-medium: 0 3px 6px rgba($dark-gray, 0.15); $shadow-image-hover: 0 4px 20px rgba(#000, 0.15); // Forms diff --git a/about.md b/about.md index 2d890ac..de48edc 100644 --- a/about.md +++ b/about.md @@ -12,7 +12,7 @@ main_nav: true nav_order: 1 --- -![Jonas Kim]({{ site.baseurl }}/assets/my-profile.png){:.profile} +![Jonas Kim]({{ site.baseurl }}/assets/my-profile.png){:.profile loading="eager"} ## Hello, I'm Jonas Kim diff --git a/css/search.css b/css/search.css index 0889322..e2e91cb 100644 --- a/css/search.css +++ b/css/search.css @@ -59,16 +59,17 @@ border-bottom: none; } -.search-result h3 { +.search-result h2 { + font-size: 1.44em; /* the h3 size it had before it became an h2 */ margin: 0 0 0.5em; } -.search-result h3 a { +.search-result h2 a { color: #333; text-decoration: none; } -.search-result h3 a:hover { +.search-result h2 a:hover { color: #2980b9; } diff --git a/docs/tech-doc.md b/docs/tech-doc.md index a9048f6..ead193e 100644 --- a/docs/tech-doc.md +++ b/docs/tech-doc.md @@ -112,7 +112,7 @@ Pages가 그대로 서빙.** 글이 어느 내비 탭에 들어갈지를 정한다. 넷 다 전용 페이지와 탭을 가지며, 글이 없는 유형은 빈 상태 문구를 띄운다(아래 "내비게이션은 코드가 아니라 데이터가 만든다" 참조). - **1단계 (주제)** — `_data/post_conventions.yml`의 `topics` 중 하나. 주제마다 표지 이미지가 - 정해져 있고, 맞는 주제가 없으면 먼저 거기에 추가한다. 목록에 없는 값은 린트가 막는다. + 정해져 있고(Insights는 유형 표지 `insights.jpg`가 우선), 맞는 주제가 없으면 먼저 거기에 추가한다. 목록에 없는 값은 린트가 막는다. 이 둘과 **날짜**가 합쳐져 출력 경로(=URL)가 된다: @@ -128,7 +128,7 @@ _site/paper-reviews/language-models/2025/01/23/<slug>.html 공백을 `-`로 바꾸므로, 앵커에 쓰는 `slugify`와 결과가 같다. 그래서 **이미 게시된 글의 카테고리나 날짜를 바꾸면 URL이 바뀐다** — 외부에서 걸린 링크와 -검색 색인이 깨지므로 함부로 고치지 않는다(§9의 "날짜 드리프트" 참조). +검색 색인이 깨지므로 함부로 고치지 않는다(§9의 "파일명 날짜와 `date:`" 참조). > **옛 URL 리디렉션.** 예전 규칙(`:categories`)은 공백을 그대로 둬 `Paper Reviews` 글이 > `/paper%20reviews/…`로 게시됐다. `_plugins/legacy_urls.rb`가 옛 규칙으로 계산한 경로가 지금 @@ -221,7 +221,7 @@ HTML에 내보낸다. 그러면 브라우저에서 MathJax 3(`_includes/head.htm ### 단일 `$`가 깨지는 이유 (꼭 알아야 함) kramdown은 단일 `$`를 수식으로 **취급하지 않는다**. `$x_i + y_j$`는 그냥 일반 텍스트로 본다. -문제는 마크다운 강조 문법(`_..._` → 기울임, `*...*` → 굵게)이 수식 처리보다 **먼저** 돈다는 +문제는 마크다운 강조 문법(`_..._`와 `*...*` → 기울임, `**...**` → 굵게)이 수식 처리보다 **먼저** 돈다는 점이다. 그래서 `$` 안에 짝지어진 `_`나 `*`가 있으면 그게 `<em>`/`<strong>` 태그로 바뀌어, MathJax에는 이미 망가진 입력이 전달된다(위 그림의 빨간 경로). 예를 들어 `$a*b*c$`가 `$a<em>b</em>c$`가 되어버린다. @@ -290,8 +290,8 @@ MathJax 설정은 `head.html`에 있고 `{% if page.use_math %}`로 감싸 **프 - 질의가 두 단어 이상이면 매치마다 창을 만들어 보고 **서로 다른 단어를 가장 많이 담는 창**을 고른다. 첫 매치만 쓰면 "vibe coding"에서 멀리 떨어진 `coding` 하나만 걸려 반쪽짜리 발췌가 나온다. - - 단어 중간에서 자르지 않으려 공백을 찾지만 **12자 안에서만** 찾는다. 한국어는 어절 사이 - 공백이 없어 무제한으로 찾으면 매치를 지나쳐 창을 삼킨다. 상한이 있으면 글자 단위 절단으로 + - 단어 중간에서 자르지 않으려 공백을 찾지만 **12자 안에서만** 찾는다. URL·수식·한자 연속처럼 + 공백 없이 긴 구간에서 무제한으로 찾으면 매치를 지나쳐 창을 삼킨다. 상한이 있으면 글자 단위 절단으로 자연스럽게 내려앉는다. - 미들웨어가 돌려주는 값은 라이브러리가 **HTML로 삽입**하므로, 이 파일이 직접 escape하고 `<mark>`만 스스로 넣는다. @@ -382,11 +382,16 @@ Bourbon → base/ → Neat → _layout → _post → _tags → _syntax(Rouge 코 canonical이 있는지, 설명문·제목이 중복되지 않는지 등을 확인한다. **검사 목록의 정본은 스크립트 자체다** — 각 검사 옆 주석에 그 검사가 왜 있는지가 적혀 있다. htmlproofer가 보지 않는 영역을 메우는 게 목적이다. 인자로 빌드 경로를 받고, 없으면 `_site`를 본다. -- **글 원본 린트** — `script/lint-posts.rb`가 `_posts`의 마크다운을 빌드 전에 검사한다. + 플러그인 훅이 글에 남겨야 할 결과(옛 URL 리디렉션, 수정일, 이전·다음 링크, 이미지 `loading`·`width`, + 표 스크롤 컨테이너), kramdown이 읽지 못해 닫는 태그가 글자로 나간 블록 HTML, 피드 상한은 + `script/check-plugin-output.rb`가 Jekyll로 글을 직접 읽어 + 확인하고, `validate-site.sh`가 이를 실행한다. 단위 테스트는 플러그인 함수만 보므로 훅이 빠져도 + 빌드는 통과하기 때문이다. +- **글 원본 린트** — `script/lint-posts.rb`가 `_posts`(하위 폴더 포함)의 마크다운을 빌드 전에 검사한다. 글은 `.md` 파일이어야 한다. 프런트매터는 `_data/post_conventions.yml`과 대조한다(카테고리·표지·금지 키·통제 태그·폐기된 태그 이름·태그 형식·번역본 파일명·논문 글의 `paper_author`). 빌드된 HTML에서는 더 보이지 않는 결함도 잡는다: 헤더와 열 수가 어긋난 표, 2단 헤더를 펴다 생긴 하위 헤더 행, `$$` 안의 `\_`, 빈 줄 없는 디스플레이 - 수식(목록 안 포함), `\\(`·`\\[`로 쓴 수식과 단일 `$` 옆의 TeX, 짝이 맞지 않는 `$$`, 본문 H1. 통화 `$$`가 연 수식은 그 사이에 한글이 있을 때만 잡는다. 검사 목록의 정본은 스크립트와 `test/test_lint_posts.rb`다. + 수식(목록 안 포함), `\\(`·`\\[`로 쓴 수식과 단일 `$` 옆의 TeX, 짝이 맞지 않는 `$$`, `use_math: true` 없는 수식, 표 행의 `$$` 안에 있는 `|`(`\|` 포함), `{% raw %}` 밖의 Liquid 태그(코드 블록의 `{{ }}`는 빈칸으로 렌더된다), HTML이 아닌 `<think>` 같은 태그(브라우저가 태그를 지운다), kramdown이 펜스로 읽지 않는 코드 펜스(정보 문자열의 공백, 닫히지 않은 펜스), 글마다 대소문자를 다르게 쓴 태그, 본문 H1. 프런트매터에서는 첫 줄의 `---`, 평문 `description`(마크업·링크 없음), 파일명의 `YYYY-MM-DD-`, 파일명 날짜보다 늦지 않은 `date:`도 본다. `_config.yml`의 `future: true`는 정오로 날짜를 적은 당일 글이 한국 오전 머지에서 빠지지 않게 하고, `strict_filters`는 등록되지 않은 필터를 빌드 오류로 만든다. 글 사이의 규칙으로는 번역 짝(`<slug>.md`와 `<slug>-en.md`가 같은 `translation_id`)이 둘 다 있는지 본다. 통화 `$$`가 연 수식은 그 사이에 한글이 있을 때만 잡는다. 검사 목록의 정본은 스크립트와 `test/test_lint_posts.rb`다. - **단위 테스트** — `test/`가 `_plugins/`의 순수 로직(플러그인당 한 파일)과 글 원본 린트를 minitest로 검사한다. `ruby test/run_all.rb`로 돌리며 CI에서 빌드보다 먼저 실행된다. 플러그인은 모든 페이지의 메타데이터를 만들기 때문에, 여기서 조용히 동작이 바뀌면 사이트 diff --git a/js/search.js b/js/search.js index b5fb2c2..5386c23 100644 --- a/js/search.js +++ b/js/search.js @@ -171,7 +171,7 @@ json: searchInput.getAttribute('data-index'), searchResultTemplate: '<div class="search-result">' + - '<h3><a href="{url}">{title}</a></h3>' + + '<h2><a href="{url}">{title}</a></h2>' + '<p class="result-meta"><span class="post-date">{date}</span>' + '<span class="post-category">{category}</span></p>' + '<p class="result-snippet">{content}</p>' + diff --git a/paper-reviews.md b/paper-reviews.md index f657f92..e5be43b 100644 --- a/paper-reviews.md +++ b/paper-reviews.md @@ -1,4 +1,5 @@ --- +lang: en layout: page title: Paper Reviews permalink: /paper-reviews/ diff --git a/paper-summaries.md b/paper-summaries.md index 3140b19..93f9a8c 100644 --- a/paper-summaries.md +++ b/paper-summaries.md @@ -1,4 +1,5 @@ --- +lang: en layout: page title: Paper Summaries permalink: /paper-summaries/ diff --git a/script/check-plugin-output.rb b/script/check-plugin-output.rb new file mode 100644 index 0000000..cc355c3 --- /dev/null +++ b/script/check-plugin-output.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true +# +# Checks that the _plugins/ hooks that change a post's page left their mark, +# and that kramdown parsed the post's block HTML. +# +# test/ covers the plugins' functions, not the Jekyll hooks that apply them, and +# with a hook gone the build still passes. Jekyll itself reads the posts here, +# so which posts exist (published, future, folders, slug) is its answer, not a +# second guess from the file names. +# +# Usage: bundle exec ruby script/check-plugin-output.rb <site_dir> +# Prints one line per problem; exits 1 when there is any. + +require "jekyll" + +site_dir = File.expand_path(ARGV.fetch(0)) +root = File.expand_path("..", __dir__) +config = Jekyll.configuration("source" => root, "destination" => site_dir, "quiet" => true) +site = Jekyll::Site.new(config) +site.reset +site.read +posts = site.posts.docs +abort "no posts read from #{root}/_posts" if posts.empty? + +problems = [] +lang = ->(post) { post.data["lang"] || site.config["lang"] } +per_lang = posts.group_by(&lang).transform_values(&:size) +# Where Jekyll writes a URL: a directory one to index.html, a bare one to .html. +built = lambda do |url| + file = url.end_with?("/") ? "#{url}index.html" : File.extname(url).empty? ? "#{url}.html" : url + File.join(site_dir, Jekyll::URL.unescape_path(file)) +end + +limit = site.config.dig("feed", "posts_limit") || 10 # jekyll-feed's default +# The feed is sorted by `date:`, the paper's date, so past the cap a new review +# of an old paper never reaches it. +problems << "feed.posts_limit is #{limit} for #{posts.size} posts; raise it in _config.yml" if posts.size > limit + +posts.each do |post| + name = File.basename(post.path) + path = built.call(post.url) + next problems << "#{name}: not built at #{post.url}" unless File.file?(path) + + html = File.read(path, encoding: "UTF-8") + + # legacy_urls.rb: a stub at the pre-slug URL of a post published before the change. + legacy = Jekyll::URL.new(template: LegacyUrls::TEMPLATE, placeholders: post.url_placeholders).to_s + if LegacyUrls.legacy?(name) && legacy != post.url && !File.file?(built.call(legacy)) + problems << "#{name}: no redirect stub at #{Jekyll::URL.unescape_path(legacy)}" + end + + # posted_date.rb: a modified time no earlier than a later file-name date. + if (posted = PostedDate.last_modified(name, post.date)) + modified = html[/article:modified_time" content="([^"]+)"/, 1] + unless modified && Date.parse(modified) >= posted.to_date + problems << "#{name}: modified time #{modified.inspect}, posted #{posted.to_date}" + end + end + + # related_posts.rb: previous/next links, given another post in the language. + if per_lang[lang.call(post)] > 1 && !html.include?(%(class="post-nav")) + problems << "#{name}: no previous/next links" + end + + imgs = html.scan(/<img\b[^>]*>/) + # lazy_images.rb + problems << "#{name}: an image without loading=" if imgs.any? { |t| !t.include?("loading=") } + # image_dimensions.rb: every local image whose header it can read. + unsized = imgs.filter_map { |t| t.match(ImageDimensions::SIZELESS_IMG)&.[](2) }.select do |src| + file = ImageDimensions.source_file(src, root, site.baseurl) + File.file?(file) && ImageDimensions.read(File.binread(file)) + end + problems << "#{name}: no width=/height= on #{unsized.first}" if unsized.any? + # scrollable_tables.rb + if html.scan("<table").size > html.scan(%(class="table-container")).size + problems << "#{name}: a table outside a scroll container" + end + + # Not a hook: block HTML kramdown could not parse (a one-line <div>…</div>, a + # <summary> without markdown="span") ships its closing tag as text and leaves + # the element open over the rest of the post. A closing tag the author + # escaped on purpose (\</div\> or </div>, outside code) is text too. + prose = html[%r{<article class="post-content">.*</article>}m].to_s.gsub(%r{<(pre|code|script)\b.*?</\1>}m, "") + source = post.content.gsub(/^(`{3,}|~{3,}).*?^\1/m, "").gsub(/`[^`\n]*`/, "") + broken = %w[blockquote center details div figcaption figure section span summary].find do |tag| + prose.scan("</#{tag}>").size > source.scan(%r{\\</#{tag}\\?>|</#{tag}>}).size + end + problems << "#{name}: </#{broken}> shows as text; put block HTML tags on their own lines" if broken +end + +puts problems +exit(problems.empty? ? 0 : 1) diff --git a/script/lint-posts.rb b/script/lint-posts.rb index f65bea4..85ec4d6 100644 --- a/script/lint-posts.rb +++ b/script/lint-posts.rb @@ -6,22 +6,27 @@ # currency sign read as math is just math. Each check below is a defect that # shipped in a post here. # -# Usage: ruby script/lint-posts.rb [post.md ...] (default: every _posts/*.md) +# Usage: ruby script/lint-posts.rb [post.md ...] (default: every file under _posts/) # Exits 1 when any post breaks a rule. require "yaml" require "date" +require "time" +require "set" module PostLint ROOT = File.expand_path("..", __dir__) + # Dates without an offset are read in the zone the site builds in, as Jekyll does. + ENV["TZ"] = YAML.safe_load_file(File.join(ROOT, "_config.yml"))["timezone"] || "UTC" # One parsed post: front matter, body, and the line the body starts on, so # findings point at the source line. Post = Struct.new(:path, :data, :body, :body_line) def self.parse(path, text = File.read(path)) - parts = text.split(/^---\s*$\n?/, 3) - raise ArgumentError, "#{path}: no front matter" unless parts.size == 3 && parts[0].strip.empty? + parts = text.delete_prefix("\uFEFF").split(/^---[ \t]*\r?$\n?/, 3) # Jekyll reads bom|utf-8 + # Jekyll reads front matter only when `---` is the first line. + raise ArgumentError, "#{path}: no front matter" unless parts.size == 3 && parts[0].empty? data = YAML.safe_load(parts[1], permitted_classes: [Date, Time]) || {} Post.new(path, data, parts[2], parts[1].count("\n") + 3) @@ -29,12 +34,72 @@ def self.parse(path, text = File.read(path)) # Returns findings as "line: message" strings; empty when the post passes. def self.check(post, spec) - front_matter(post, spec) + tags(post, spec) + translation(post) + body(post) + front_matter(post, spec) + tags(post, spec) + translation(post, spec) + body(post) + use_math(post) + liquid(post) + end + + # Jekyll renders a post as Liquid first, so a prompt template's {{ question }} + # in a code block renders as nothing. The Kimi K2 post shipped that way. + def self.liquid(post) + return [] if post.data["render_with_liquid"] == false + + raw = false + post.body.split("\n", -1).each_with_index.filter_map do |line, i| + # Token by token: a line may close a raw span and open a bare tag. + hit = false + line.split(/(\{%-?\s*(?:end)?raw\s*-?%\})/).each do |part| + if (tag = part[/\A\{%-?\s*((?:end)?raw)\s*-?%\}\z/, 1]) + raw = tag == "raw" + elsif !raw && part.match?(/\{\{|\{%/) + hit = true + end + end + "#{post.body_line + i}: Liquid tag in the body; unless meant as Liquid, wrap it in {% raw %} ... {% endraw %}" if hit + end + end + + # MathJax loads only on posts that ask for it; without it $$...$$ ships as TeX. + def self.use_math(post) + # Code spans line by line, as body() does: one odd backtick must not pair + # with another paragraphs later and hide the math between them. + prose = post.body.gsub(/^\s*(`{3,}|~{3,}).*?^\s*\1/m, "").gsub(/(`+)[^\n]*?\1/, "") + prose.include?("$$") && post.data["use_math"] != true ? ["1: the post has $$ math but no use_math: true"] : [] + end + + # Across posts: a translation is a pair, one `<slug>.md` and one `<slug>-en.md` + # with translation_id <slug>. Half a pair lists the English post as its own + # article and shows no language switcher. + def self.pairs(posts, spec) + found = Hash.new { |h, k| h[k] = [] } + slug = ->(post) { File.basename(post.path, ".md").sub(/\A\d{4}-\d{2}-\d{2}-/, "") } + posts.each do |post| + found[post.path] << "1: an -en post must set translation_id" if slug.call(post).end_with?("-en") && !post.data["translation_id"] + end + posts.select { |p| p.data["translation_id"] }.group_by { |p| p.data["translation_id"].to_s }.each do |id, group| + next if group.map { |p| p.data["lang"].to_s }.sort == spec["languages"].sort + + group.each { |post| found[post.path] << "1: translation_id #{id} is not on one post in each of #{spec["languages"].join(", ")}" } + end + found end def self.front_matter(post, spec) data = post.data found = (spec["forbidden_keys"] & data.keys).map { |k| "1: front matter key `#{k}` is not allowed" } + # It ships as plain text (meta, feed, home card); markup would show literally. + if data["description"].to_s.match?(/\$\$|\*\S[^*]*\*|`|\]\(|<[a-zA-Z\/]|\{[{%]/) + found << "1: description is plain text; no $$, *emphasis*, backticks, links or tags" + end + + # Jekyll drops a file without the date prefix, and builds a `date:` past the + # file's day into a permanent URL. The file-name date is the posting day. + if (posted = File.basename(post.path)[/\A(\d{4}-\d{2}-\d{2})-/, 1]) + # Jekyll reads a quoted date too, and builds in UTC (_config.yml). + date = data["date"].is_a?(String) ? (Time.parse(data["date"]) rescue nil) : data["date"] + day = date.respond_to?(:to_time) && date.to_time.utc.to_date + found << "1: date: #{day} (UTC) is after the file name's #{posted}" if day && day > Date.parse(posted) + else + found << "1: the file name starts with YYYY-MM-DD-, or Jekyll skips the post" + end type, topic = Array(data["categories"]) unless Array(data["categories"]).size == 2 && spec["types"].key?(type) && spec["topics"].key?(topic) @@ -75,12 +140,31 @@ def self.tags(post, spec) found end + # Tags that differ only in case are one tag to related_posts.rb but two + # sections, with one id, on the tags page. + def self.tag_cases(posts, spec) + found = Hash.new { |h, k| h[k] = [] } + spellings = (spec["tags"]["controlled"].map { |t| [t, nil] } + + posts.flat_map { |p| Array(p.data["tags"]).map { |t| [t.to_s, p] } }).group_by { |t, _| t.downcase } + spellings.each_value do |uses| + forms = uses.map(&:first).uniq + next if forms.size == 1 + + uses.each { |tag, post| found[post.path] << "1: tag #{tag} is also written #{(forms - [tag]).join(", ")}" if post } + end + found + end + # A translation lives at <original-slug>-en.md and both posts use that slug as # their translation_id. The pairing itself runs on the id alone (the language # switcher, _plugins/translations.rb); the file names keep a pair findable. - def self.translation(post) + def self.translation(post, spec) id = post.data["translation_id"].to_s return [] if id.empty? + # The switcher and hreflang read `lang` as written, and the switcher knows + # only these two; without it a Korean post links to itself as "English". + langs = spec["languages"] + return ["1: a post with translation_id must set lang (#{langs.join(" or ")})"] unless langs.include?(post.data["lang"]) slug = File.basename(post.path, ".md").sub(/\A\d{4}-\d{2}-\d{2}-/, "") expected = post.data["lang"] == "en" ? "#{id}-en" : id @@ -89,10 +173,26 @@ def self.translation(post) FENCE = /\A\s*(`{3,}|~{3,})/ + # kramdown passes any <name> through as an element, and the browser drops a + # tag it does not know: "<think> </think>" vanished from a prompt template in + # the DeepSeek-R1 post. A prompt token named like a form element (<input>, + # <output>, <template>) or <s> fares no better, so this is the HTML a post + # writes, not all of HTML. + HTML_ELEMENTS = %w[ + a abbr audio b blockquote br caption center cite code col colgroup dd del details + div dl dt em figcaption figure h1 h2 h3 h4 h5 h6 hr i iframe img ins kbd li mark + ol p picture pre script section small source span strong sub summary sup svg + table tbody td tfoot th thead tr ul video wbr + circle defs g line path polygon polyline rect text tspan use + ].to_set # the last row: an inline <svg>'s children + # Elements that mean nothing bare: <img> or <audio> alone is a token, not media. + NEEDS_ATTRIBUTES = %w[a audio iframe img script source video].to_set + def self.body(post) found = [] lines = post.body.split("\n", -1) fence = nil # the marker that opened the code fence we are in + fence_line = nil block = nil # the line a still-open multi-line $$ block started on span = nil # the line an inline $$ span wrapped onto the next line started on lines.each_with_index do |line, i| @@ -100,6 +200,10 @@ def self.body(post) if (m = line.match(FENCE)) if fence.nil? fence = m[1] + fence_line = n + # kramdown's GFM fence takes one word after the marker; with more it + # renders the whole block as inline code. + found << "#{n}: code fence info has a space; write just the language" if line.strip.match?(/\A(`{3,}|~{3,})\s*\S+\s+\S/) elsif line.strip.match?(/\A#{Regexp.escape(fence[0])}{#{fence.size},}\z/) fence = nil end @@ -131,16 +235,23 @@ def self.body(post) span = (span ? nil : n) if odd found << "#{n}: H1 in the body; the layout renders the title" if line.match?(/\A# \S/) + # A whole tag, outside code and math: `i<j` and `$$a<b$$` are text. + text = unquote(line).gsub(/`[^`]*`/, "").gsub(/\$\$.*?\$\$/, "") + tags = text.scan(%r{(?<!\\)<(/?)([a-zA-Z][\w-]*)(\s[^<>]*)?/?>}) + if (tag = tags.find { |close, name, attrs| !HTML_ELEMENTS.include?(name.downcase) || (close.empty? && attrs.to_s.strip.empty? && NEEDS_ATTRIBUTES.include?(name.downcase)) }&.[](1)) + found << "#{n}: <#{tag}> is read as HTML, so it does not show as text; write it in `code` or as \\<#{tag}\\>" + end found.concat(math(line, n, lines, i)) found.concat(table(lines, i, n)) if table_start?(lines, i) end found << "#{block}: $$ block never closed" if block + found << "#{fence_line}: code fence never closed (a nested fence closes the outer one)" if fence found << "#{span}: unbalanced $$ in the paragraph" if span found end # Brace groups whose content is text, not math: escapes and Korean belong there. - TEXT_GROUP = /\\(?:text\w*|mathrm|operatorname)\{/ + TEXT_GROUP = /\\(?:text\w*|mathrm|mathtt|operatorname)\{/ def self.without_text_groups(math) out = +"" @@ -232,6 +343,12 @@ def self.table(lines, i, n) if first && first[0].empty? && first.none? { |c| c.match?(/\A[-+]?[\d.,]+%?\z/) } found << "#{n + 2}: table row under the header is a second header row; merge it into the header" end + ([lines[i]] + rows).each_with_index do |row, k| + # kramdown splits the cell at |, and turns \| into | (a norm into an absolute value). + next unless row.gsub(/`[^`]*`/, "").scan(/\$\$(.*?)\$\$/).flatten.any? { |m| m.include?("|") } + + found << "#{k.zero? ? n : n + 1 + k}: a | inside $$ in a table breaks it; write \\mid, \\lvert...\\rvert or \\lVert...\\rVert" + end rows.each_with_index do |row, k| count = cells(row).size found << "#{n + 2 + k}: table row has #{count} cells, the header #{header.size}" if count > header.size @@ -246,15 +363,32 @@ def self.spec if $PROGRAM_NAME == __FILE__ spec = PostLint.spec - paths = ARGV.empty? ? Dir[File.join(PostLint::ROOT, "_posts/*.md")].sort : ARGV - failed = 0 - paths.each do |path| + # Every file Jekyll might read as a post, subfolders included; any but .md is + # a finding, so a post cannot dodge the lint by its extension. + paths = ARGV.empty? ? Dir[File.join(PostLint::ROOT, "_posts/**/*")].select { |f| File.file?(f) }.sort : ARGV + abort "no posts found under #{PostLint::ROOT}/_posts" if paths.empty? + parsed = {} + findings_by_path = paths.to_h do |path| findings = begin - PostLint.check(PostLint.parse(path), spec) + if File.extname(path) == ".md" + post = parsed[path] = PostLint.parse(path) + PostLint.check(post, spec) + else + ["1: a post is a .md file"] + end rescue StandardError, Psych::Exception => e ["1: could not be checked: #{e.message}"] end + [path, findings] + end + # Pairs need every post; a run on named files checks those files alone. + if ARGV.empty? + PostLint.pairs(parsed.values, spec).each { |path, f| findings_by_path[path].concat(f) } + PostLint.tag_cases(parsed.values, spec).each { |path, f| findings_by_path[path].concat(f.uniq) } + end + failed = 0 + findings_by_path.each do |path, findings| next if findings.empty? failed += 1 diff --git a/script/validate-site.sh b/script/validate-site.sh index 9b45234..6e2dc6e 100755 --- a/script/validate-site.sh +++ b/script/validate-site.sh @@ -34,6 +34,7 @@ if [ ! -d "$SITE" ]; then echo "no such directory: $SITE" >&2 exit 1 fi +SITE=$(cd "$SITE" && pwd) # the plugin-output check runs from $ROOT if [ ! -f "$CONFIG" ]; then echo "no _config.yml at $CONFIG" >&2 @@ -99,8 +100,10 @@ fi # jekyll-feed takes each entry's <author> from the post's `author`, falling back # to the site's. Paper posts name the paper's organisation in `paper_author`; one # that says `author:` instead credits that organisation with this blog's post. -SITE_AUTHOR=$(sed -nE 's/^author:[[:space:]]*"?([^"]*[^"[:space:]])"?[[:space:]]*$/\1/p' "$CONFIG" | head -1) -if [ -f "$SITE/feed.xml" ]; then +SITE_AUTHOR=$(ruby -ryaml -e 'print YAML.safe_load_file(ARGV[0])["author"]' "$CONFIG") +if [ -z "$SITE_AUTHOR" ]; then + fail "_config.yml sets no author:, so every feed entry ships an empty <author>" +elif [ -f "$SITE/feed.xml" ]; then others=$(grep -oE '<author><name>[^<]*' "$SITE/feed.xml" | sed 's/<author><name>//' | grep -vxF "$SITE_AUTHOR" | sort -u || true) if [ -n "$others" ]; then @@ -149,7 +152,9 @@ dangling="" while IFS= read -r f; do [ -n "$f" ] || continue to=$(grep -m1 -oE 'http-equiv="refresh" content="0; url=[^"]+"' "$f" | sed -E 's/.*url=//; s/"$//') - rel=$(ruby -ruri -e 'print URI.decode_uri_component(URI(ARGV[0]).path.delete_prefix("/"))' "$to") + case "$to" in "$BASE_URL"/*|/*) ;; *) continue ;; esac # redirect_to another site + # Served as Jekyll writes it: a directory URL from index.html, a bare one from .html. + rel=$(ruby -ruri -e 'p = URI.decode_uri_component(URI(ARGV[0]).path.delete_prefix("/")); print(p.end_with?("/") || p.empty? ? "#{p}index.html" : File.extname(p).empty? ? "#{p}.html" : p)' "$to") [ -f "$SITE/$rel" ] || dangling="$dangling$f -> $to"$'\n' done <<< "$redirects" if [ -n "$dangling" ]; then @@ -159,6 +164,18 @@ else pass "$(printf '%s\n' "$redirects" | grep -c .) redirect stub(s), every target exists" fi +# --- plugin hooks ------------------------------------------------------------ +# What each _plugins/ hook adds to a post, checked against Jekyll's own reading +# of the posts; the script says which hook and why. +if hooks=$(cd "$ROOT" && bundle exec ruby script/check-plugin-output.rb "$SITE"); then + pass "every post carries what the plugin hooks add, and the feed has room for all" +elif [ -n "$hooks" ]; then + fail "plugin output missing:" + printf '%s\n' "$hooks" | sed 's/^/ /' >&2 +else + fail "the plugin-output check could not run (error above)" +fi + # --- rendered pages ---------------------------------------------------------- # Only Jekyll-rendered documents; the search-engine ownership-verification files # also end in .html but are plain text by design, and redirect stubs (above) are @@ -210,6 +227,7 @@ fi # A heading outline that jumps h2 -> h4 breaks screen-reader navigation. The usual # cause is a "### TL;DR" above the "##" sections it precedes. +skips_ok=1 skips=$(ruby -e ' files = STDIN.read.split("\n").reject(&:empty?) tag = /<[^>]+>/ @@ -223,8 +241,10 @@ skips=$(ruby -e ' prev = lvl end end -' <<< "$pages") -if [ -n "$skips" ]; then +' <<< "$pages") || skips_ok=0 +if [ "$skips_ok" -eq 0 ]; then + fail "the heading-outline check could not run (error above)" +elif [ -n "$skips" ]; then fail "heading outlines that skip a level:" printf '%s\n' "$skips" | sed 's/^/ /' >&2 else @@ -285,27 +305,40 @@ fi # `listed` filter shows the English twin again, and htmlproofer is blind to it. SITE_LANG=$(sed -nE 's/^lang:[[:space:]]*"?([a-z-]+)"?.*/\1/p' "$CONFIG" | head -1) BASE_PATH=$(sed -nE "s/^baseurl:[[:space:]]*['\"]?([^'\"[:space:]]*)['\"]?.*/\1/p" "$CONFIG" | head -1) -listed_twins=$(ruby -e ' +# Pages that show the language switcher, i.e. built translations. +TRANSLATED=$(grep -rl --include='*.html' 'class="language-switcher"' "$SITE" | grep -c .) +twins_ok=1 +listed_twins=$(TRANSLATED="$TRANSLATED" ruby -e ' site, site_lang, base, base_path = ARGV hreflang = /<link rel="alternate" hreflang="#{Regexp.escape(site_lang)}"/ twins = Dir.glob("#{site}/**/*.html").filter_map do |f| - html = File.read(f) + html = File.read(f, encoding: "UTF-8") lang = html[/<html lang="([^"]+)"/, 1] f.delete_prefix(site) if lang && lang != site_lang && html.match?(hreflang) end - listings = Dir.glob(["#{site}/index.html", "#{site}/page/*/index.html", "#{site}/*/index.html"]) + # No twin found while translations exist means the hreflang links are gone, + # and the scan below would pass checking nothing. + puts "no page carries hreflang=\"#{site_lang}\" though posts are translated" if twins.empty? && ENV["TRANSLATED"].to_i.positive? + # Every rendered page but the posts themselves, whose previous/next links may + # name a twin, and redirect stubs. + listings = Dir.glob("#{site}/**/*.html").reject do |f| + html = File.read(f, encoding: "UTF-8") + !html.include?("<!DOCTYPE html>") || html.include?(%(property="og:type" content="article")) || html.include?(%(http-equiv="refresh")) + end listings.each do |f| - html = File.read(f) + html = File.read(f, encoding: "UTF-8") twins.each do |t| hrefs = [t, "#{base_path}#{t}", "#{base}#{base_path}#{t}"].map { |u| %(href="#{u}") } puts "#{f.delete_prefix(site)} -> #{t}" if hrefs.any? { |h| html.include?(h) } end end -' "$SITE" "$SITE_LANG" "$BASE_URL" "$BASE_PATH") +' "$SITE" "$SITE_LANG" "$BASE_URL" "$BASE_PATH") || twins_ok=0 if [ -z "$SITE_LANG" ]; then fail "could not read 'lang:' from $CONFIG" +elif [ "$twins_ok" -eq 0 ]; then + fail "the translation-listing check could not run (error above)" elif [ -n "$listed_twins" ]; then - fail "listing pages that link a translation besides its original:" + fail "listing pages that link a translation besides its original, or no hreflang at all:" printf '%s\n' "$listed_twins" | sed 's/^/ /' >&2 else pass "listing pages show each translated article once" diff --git a/tech-guides.md b/tech-guides.md index 86d4574..44164d7 100644 --- a/tech-guides.md +++ b/tech-guides.md @@ -1,4 +1,5 @@ --- +lang: en layout: page title: Tech Guides permalink: /tech-guides/ diff --git a/test/test_image_dimensions.rb b/test/test_image_dimensions.rb index e9fbde7..4e88e09 100644 --- a/test/test_image_dimensions.rb +++ b/test/test_image_dimensions.rb @@ -32,6 +32,7 @@ def test_reads_an_svg_root_size_or_its_view_box assert_equal [860, 430], ImageDimensions.read(%(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1" width="860" height="430"><path stroke-width="2"/>)) assert_equal [840, 440], ImageDimensions.read(%(<?xml version="1.0"?>\n<svg width="100%" viewBox="0 0 840 440">)) assert_nil ImageDimensions.read(%(<svg width="100%">)) + assert_equal [841, 440], ImageDimensions.read(%(<svg viewBox="0 0 840.6 440.2">)) end def test_reads_a_progressive_jpeg diff --git a/test/test_lint_posts.rb b/test/test_lint_posts.rb index b838c4c..cb66090 100644 --- a/test/test_lint_posts.rb +++ b/test/test_lint_posts.rb @@ -16,6 +16,7 @@ class TestLintPosts < Minitest::Test categories: ["Paper Reviews", "Language-Models"] tags: ["Sliding-Window-Attention", "Efficient-Attention"] cover: /assets/images/language-models.jpg + use_math: true YAML def lint(body = "Text.\n", front: FRONT, path: "_posts/2025-02-27-mistral-7b.md") @@ -33,7 +34,9 @@ def test_a_conforming_post_passes def test_rejects_author_and_a_person_list_as_paper_author found = lint(front: FRONT.sub('paper_author: "Mistral AI"', 'author: "Preston Rasmussen et al."')) assert_finding(/key `author`/, found) + assert_finding(/key `last_modified_at`/, lint(front: "#{FRONT}last_modified_at: 2026-01-01\n")) assert_finding(/paper_author must name/, found) + assert_finding(/paper_author must name/, lint(front: FRONT.sub('"Mistral AI"', '"Rasmussen et al."'))) end def test_rejects_an_unknown_topic_and_a_wrong_cover @@ -53,6 +56,8 @@ def test_a_translation_is_named_after_its_original front = "#{FRONT}lang: en\ntranslation_id: claude-code-source-map-leak-analysis\n" assert_finding(/is named claude-code-source-map-leak-analysis-en/, lint(front: front, path: "_posts/2026-03-31-claude-code-architecture-analysis.md")) assert_equal [], lint(front: front, path: "_posts/2026-03-31-claude-code-source-map-leak-analysis-en.md") + assert_finding(/must set lang/, lint(front: "#{FRONT}translation_id: mistral-7b\n")) + assert_finding(/must set lang/, lint(front: "#{FRONT}translation_id: mistral-7b\nlang: kr\n")) end def test_math_escapes_currency_and_display_spacing @@ -76,6 +81,8 @@ def test_pipes_inside_code_and_math_are_not_cell_breaks def test_a_body_h1_outside_code assert_finding(/H1 in the body/, lint("# Title again\n")) + # A blank line after the front matter still counts toward the line number. + assert_equal ["11: H1 in the body; the layout renders the title"], lint("\n# Title again\n") assert_equal [], lint("```bash\n# a comment\n```\n") end @@ -102,6 +109,23 @@ def test_a_multi_line_display_block_is_checked_inside_and_around assert_equal [], lint("Intro\n$$x$$는 $$y$$보다 큽니다.\n") end + def test_a_non_html_tag_renders_as_nothing + assert_finding(/<think> is read as HTML/, lint("답변은 <think> 태그로 감쌉니다.\n")) + assert_finding(/<s> is read as HTML/, lint("문장 앞에 <s>를 붙입니다.\n")) + assert_finding(/<input> is read as HTML/, lint("질문은 <input>에 넣습니다.\n")) + assert_equal [], lint("모델은 \\<think> 로 시작합니다.\n") + assert_finding(/<img> is read as HTML/, lint("이미지 자리에 <img> 토큰이 들어갑니다.\n")) + assert_equal [], lint("<svg width=\"10\"><rect width=\"1\"/><path d=\"M0\"/></svg>\n") + assert_equal [], lint("<center><svg width=\"1\"></svg></center> <script src=\"https://gist.github.com/x.js\"></script>\n") + assert_equal [], lint("For $$i<j \\le n$$ and x<y z we sum.\n") + assert_equal [], lint("답변은 `<think>` 또는 \\<think\\> 태그, <br> 줄바꿈, <https://a.b> 링크.\n") + end + + def test_fences_kramdown_does_not_read_as_fences + assert_finding(/info has a space/, lint("```python title=\"a.py\"\nx\n```\n")) + assert_finding(/never closed/, lint("````md\nx\n```\n")) + end + def test_fences_close_only_on_their_own_marker assert_equal [], lint("````md\n```\n# not a heading\n```\n````\n") assert_equal [], lint("~~~\n```\n# not a heading\n~~~\n") @@ -141,6 +165,69 @@ def test_spec_macros_are_the_ones_mathjax_defines assert_equal SPEC["math"]["defined_macros"].sort, defined.sort end + def test_math_needs_use_math + without = FRONT.sub("use_math: true\n", "") + assert_finding(/no use_math/, lint("식 $$x$$입니다.\n", front: without)) + assert_finding(/no use_math/, lint("백틱(`` ` ``)입니다.\n\n식 $$x$$입니다.\n\n`a.yml`\n", front: without)) + assert_equal [], lint("```bash\necho $$\n```\n", front: without) + end + + # kramdown splits a table cell at | even inside $$, and reads \| as |. + def test_a_bar_inside_math_in_a_table + assert_finding(/inside \$\$ in a table/, lint("| a | b |\n|---|---|\n| $$p(y|x)$$ | 2 |\n")) + assert_finding(/inside \$\$ in a table/, lint("| $$p(y|x)$$ | b |\n|---|---|\n| 1 | 2 |\n")) + assert_finding(/inside \$\$ in a table/, lint("| a | b |\n|---|---|\n| $$\\|\\theta\\|$$ | 2 |\n")) + assert_equal [], lint("| Syntax | Example |\n|---|---|\n| `$$` | $$\\lvert x \\rvert$$ |\n") + end + + def test_a_tag_is_spelled_one_way + a = PostLint.parse("_posts/2026-01-01-a.md", "---\n#{FRONT}---\nText.\n") + b = PostLint.parse("_posts/2026-01-02-b.md", "---\n#{FRONT.sub('"Sliding-Window-Attention"', '"Sliding-window-Attention"')}---\nText.\n") + assert_match(/also written Sliding-Window-Attention/, PostLint.tag_cases([a, b], SPEC)[b.path].first) + assert_empty PostLint.tag_cases([a], SPEC) + end + + def test_a_translation_is_a_pair + ko = PostLint.parse("_posts/2026-01-01-x.md", "---\n#{FRONT}translation_id: x\nlang: ko\n---\nText.\n") + en = PostLint.parse("_posts/2026-01-01-x-en.md", "---\n#{FRONT}translation_id: x\nlang: en\n---\nText.\n") + bare_en = PostLint.parse("_posts/2026-01-01-x-en.md", "---\n#{FRONT}---\nText.\n") + assert_empty PostLint.pairs([ko, en], SPEC) + assert_match(/one post in each of ko, en/, PostLint.pairs([ko, bare_en], SPEC)[ko.path].first) + other_ko = PostLint.parse("_posts/2026-02-01-y.md", "---\n#{FRONT}translation_id: x\nlang: ko\n---\nText.\n") + assert_match(/one post in each of ko, en/, PostLint.pairs([ko, other_ko], SPEC)[ko.path].first) + assert_match(/must set translation_id/, PostLint.pairs([ko, bare_en], SPEC)[bare_en.path].first) + end + + def test_liquid_in_the_body_needs_raw + assert_finding(/Liquid tag/, lint("```text\nQuestion: {{ question }}\n```\n")) + assert_equal [], lint("{% raw %}\n```text\nQuestion: {{ question }}\n```\n{% endraw %}\n") + assert_finding(/Liquid tag/, lint("`{% raw %}{{ q }}{% endraw %}`와 `{{ context }}`\n")) + assert_equal [], lint("`{% raw %}{{ q }}{% endraw %}`\n") + assert_equal [], lint("`{%- raw -%}{{ q }}{%endraw%}`\n") + assert_equal [], lint("`{{ q }}`\n", front: "#{FRONT}render_with_liquid: false\n") + end + + def test_dates_and_file_names + assert_finding(/after the file name/, lint(front: "#{FRONT}date: 2025-03-01 12:00:00\n")) + assert_finding(/after the file name/, lint(front: "#{FRONT}date: \"2025-03-01 12:00:00\"\n")) + assert_finding(/after the file name/, lint(front: "#{FRONT}date: 2025-02-27 20:00:00 -0800\n")) + assert_equal [], lint(front: "#{FRONT}date: 2025-02-28 01:00:00 +0900\n") + assert_equal [], lint(front: "#{FRONT}date: 2023-10-10 17:54:58\n") + assert_finding(/YYYY-MM-DD-/, lint(path: "_posts/mistral-7b.md")) + end + + def test_description_is_plain_text + assert_finding(/plain text/, lint(front: "#{FRONT}description: \"비용은 $$O(n)$$입니다\"\n")) + assert_finding(/plain text/, lint(front: "#{FRONT}description: \"R1은 <think> 블록을 씁니다\"\n")) + assert_finding(/plain text/, lint(front: "#{FRONT}description: \"{{ question }} 자리\"\n")) + assert_equal [], lint(front: "#{FRONT}description: \"Mixtral 8*7B와 C++ [1]\"\n") + end + + def test_front_matter_starts_on_line_one + assert_raises(ArgumentError) { PostLint.parse("_posts/2025-02-27-x.md", "\n---\n#{FRONT}---\nText.\n") } + assert_equal "Mistral 7B", PostLint.parse("_posts/2025-02-27-x.md", "\uFEFF---\n#{FRONT}---\nText.\n").data["title"] + end + def test_paper_author_is_one_string_without_et_al assert_equal [], lint(front: FRONT.sub('"Mistral AI"', '"Google DeepMind, Google Research, Stanford"')) assert_finding(/paper_author/, lint(front: FRONT.sub('"Mistral AI"', '["A", "B"]'))) diff --git a/test/test_post_description.rb b/test/test_post_description.rb index b2af3a5..a0a1c25 100644 --- a/test/test_post_description.rb +++ b/test/test_post_description.rb @@ -156,9 +156,47 @@ def test_strips_code_spans_footnotes_and_inline_html assert_equal "text-to-SQL 정확도는 16.7%였습니다.", PostDescription.derive(md) end - def test_strips_math - md = "비용은 $$O(n^2)$$이고 \\(k\\)에 비례합니다.\n" - assert_equal "비용은 이고 에 비례합니다.", PostDescription.derive(md) + # Math that reads as text stays: dropping it left "비용은 이고". TeX commands, + # and characters later passes would take for markup, still go; a post whose + # first paragraph leans on them needs a hand-written description. + def test_keeps_inline_math_that_reads_as_text + md = "비용은 $$O(n^2)$$이고 $$k$$에 비례합니다.\n" + assert_equal "비용은 O(n^2)이고 k에 비례합니다.", PostDescription.derive(md) + assert_equal "손실은 로 정의됩니다.", PostDescription.derive("손실은 $$\\mathcal{L}$$로 정의됩니다.\n") + assert_equal "비교 이고 입니다.", PostDescription.derive("비교 $$p<q$$이고 $$r>s$$입니다.\n") + end + + def test_comments_are_not_prose + md = "<!--\nTODO: 서론을 다시 쓸 것\n-->\n\n{::comment}\n메모\n{:/comment}\n\n본문입니다.\n" + assert_equal "본문입니다.", PostDescription.derive(md) + assert_equal "템플릿은 {{ q }} 자리입니다.", PostDescription.derive("템플릿은 {% raw %}{{ q }}{% endraw %} 자리입니다.\n") + end + + def test_code_spans_keep_their_text + assert_equal "추론은 <think> 안에, a\\*b는 그대로.", PostDescription.derive("추론은 `<think>` 안에, `a\\*b`는 그대로.\n") + end + + def test_backslash_escapes_are_text + assert_equal "추론은 <think> 태그 안, 비용은 $2, 이름은 a_b입니다.", PostDescription.derive("추론은 \\<think\\> 태그 안, 비용은 \\$2, 이름은 a\\_b입니다.\n") + end + + # `\d+[.)]\s` read a Korean date opening as an ordered list and dropped it. + def test_a_korean_date_opening_is_prose + assert_equal "2024. 5. 14. 공개된 모델입니다.", PostDescription.derive("2024. 5. 14. 공개된 모델입니다.\n") + end + + def test_a_line_break_tag_is_a_space + assert_equal "첫 줄 둘째 줄", PostDescription.derive("첫 줄<br>둘째 줄\n") + end + + def test_a_paragraph_may_open_with_inline_html + md = "<strong>핵심</strong>은 데이터 품질입니다.\n\n둘째 문단.\n" + assert_equal "핵심은 데이터 품질입니다.", PostDescription.derive(md) + assert_equal "본문.", PostDescription.derive("<div class=\"note\">공지</div>\n\n본문.\n") + assert_equal "본문.", PostDescription.derive("<strong>핵심</strong>\n\n본문.\n") + assert_equal "핵심은 품질이고 결론도 품질", PostDescription.derive("<strong>핵심</strong>은 품질이고 결론도 <strong>품질</strong>\n") + assert_equal "본문.", PostDescription.derive("<small>그림 1: 구조</small>\n\n본문.\n") + assert_equal "본문.", PostDescription.derive("<em>공개: 저자는 <a href=\"https://aws.amazon.com\">AWS</a> 직원입니다.</em>\n\n본문.\n") end def test_joins_a_multiline_paragraph_with_spaces diff --git a/test/test_related_posts.rb b/test/test_related_posts.rb index ffd9e25..b708d3a 100644 --- a/test/test_related_posts.rb +++ b/test/test_related_posts.rb @@ -159,6 +159,13 @@ def test_returns_the_neighbours_in_date_order assert_equal "/new", newer[:id] end + def test_the_nearest_older_post_not_the_oldest + a = post("/a", date: "2023-01-01") + b = post("/b", date: "2024-01-01") + c = post("/c", date: "2025-01-01") + assert_equal "/b", RelatedPosts.adjacent(c, ordered(a, b, c))[0][:id] + end + def test_nil_at_each_end_of_the_archive old = post("/old", date: "2024-01-01") new = post("/new", date: "2026-01-01") @@ -167,7 +174,7 @@ def test_nil_at_each_end_of_the_archive assert_nil RelatedPosts.adjacent(new, list)[1] end - # The live defect: all three translation pairs share a date exactly, so + # The live defect: all three translation pairs shared a date exactly, so # Jekyll's page.previous pointed the newest Korean post at its own English # version. def test_skips_the_translation_twin_even_on_an_identical_date @@ -218,7 +225,9 @@ def test_a_lone_post_has_no_neighbours end class TestTheActualHarnessCluster < Minitest::Test - # The real front matter of the five Insights/Agentic-AI posts. + # The Insights/Agentic-AI posts' front matter as of the harness posts, cut to + # the fields ranking reads. A snapshot, not the current _posts: it pins the + # ranking rules on a realistic cluster. # # These five used to share nothing at all: the Claude Code teardown said # "Agentic-Architecture", the harness chronicle said "Agentic-Patterns", and diff --git a/test/test_search_index.rb b/test/test_search_index.rb index 70e1e85..8b1eb60 100644 --- a/test/test_search_index.rb +++ b/test/test_search_index.rb @@ -14,6 +14,10 @@ def test_unwraps_tags assert_equal "본문입니다", SearchIndex.plain_text("<p>본문입니다</p>") end + def test_drops_html_comments + assert_equal "앞 뒤", SearchIndex.plain_text("<p>앞 <!-- 메모 --> 뒤</p>") + end + # The reason this filter exists at all: `strip_html` left entities encoded and # the search page printed "음료 > 탄산음료" to the reader. def test_decodes_html_entities