2011년 9월 1일 목요일

파이어폭스 주소창의 검색 엔진 바꾸기

파이어폭스 속도가 느려서 크롬을 이용했는데, 파이어폭스 6를 써보니 크롬에 못지않은 빠른 속도를 보여준다. 크롬이 순간 검색 기능 덕분인지 웹페이지 로딩이 더 빠르긴 하지만.

그래도 이 정도 속도면 쓸만하다 싶어서 파이어폭스로 돌아와 보니, 크롬의 주소창 검색 기능이 못내 그립다. 예전엔 주소창 옆의 검색창을 가지고도 잘 썼건만, 주소창 검색의 편리함에 물들었나 보다. 파이어폭스는 주소창에 검색어를 입력하면 기본적으로 www.google.com에서 검색을 하도록 되어 있지만,  크롬처럼 검색 엔진을 쉽게 바꿀 수 없다. 난 구글의 SSL Search를 이용하고 싶은데... 어떻게 해야 할까? Beyond Technology란 블로그에서 답을 찾았다.

  1. 파이어폭스를 열고 주소창에 about:config를 입력한다. 
  2. 설정을 함부로 바꾸면 안정성이나 보안에 문제가 발생할 수 있다는 의미심장한 경고가 뜬다. 경고를 마음에 새기고 I'll be careful, I promise!를 클릭한다. 
  3. Filter 창에 keyword.URL을 입력한다.
     
  4. 검색 결과로 나온 keyword.URL 항목을 더블클릭한다.
  5. Enter string value 창이 뜨는데, 여기에 https://encrypted.google.com/search?q=를 입력한다. 다른 검색 엔진을 이용하려면 해당 URL을 입력하면 된다. 
  6. OK를 클릭하면 설정 끝! keyword.URL 항목의 Statususer set으로 변경되었고, Value에는 방금 입력한 주소가 들어가 있는 것을 확인할 수 있다.
     
  7. 이제 주소창에 검색어를 입력하고 엔터를 누르면 구글 SSL Search를 이용해서 검색 결과를 볼 수 있다.

2011년 8월 24일 수요일

C++로 간단하게 구현해보는 희소행렬

C++에서 STL을 이용하여 희소행렬(sparse matrix)을 구현한다면, 원소를 저장하기에 가장 좋은 자료형은 뭘까? 아마 map일 거다. 키를 위치 인덱스로 하고, 데이터에는 실제 값을 넣으면 된다.

STL엔 두 개의 map이 있다. 하나는 std::map이고, 다른 하나는 std::unordered_map이다. 이들의 용도는 다음과 같이 간단히 정리할 수 있다. 킷값에 따라 정렬해주는 map이 필요하다면 std::map을 사용하고, 빠른 검색이 필요하다면 std::unordered_map을 사용한다. 우린 키를 정렬할 필요는 없으므로, std::unordered_map을 이용하기로 하자.

이제 2차원 인덱스를 표현할 자료형을 결정해야 한다. C++의 내장 배열을 사용하자니 번거롭고, std::vector를 사용하자니 너무 무겁다. 2011년 8월 12일에 제정된 새로운 C++ 표준에선 std::array를 정의하고 있다. 이건, 고정된 크기의 std::vector라고 생각하면 되는데, std::vector보다 가볍고 C++ 내장 배열보다 편리하다. 행렬의 인덱스는 길이가 2로 고정되므로 std::array는 인덱스의 자료형에 딱 맞는 컨테이너이다.

이제 구현을 해보자. 먼저, 필요한 헤더를 불러온다.
// smat.cc
#include <array>
#include <cstdlib>
#include <iostream>
#include <numeric>
#include <unordered_map>
cstdlib 헤더는 main 함수의 반환 값으로 사용할 EXIT_SUCCESS 매크로를 정의가 들어 있고, numeric 헤더는 해시 값 생성에 사용할 accumulate 함수의 선언이 들어 있다.

다음으로 할 일은 함수자(functor)를 만드는 것이다. std::unordered_map해시표를 사용하는 데, 키로부터 해시 값을 계산해서 해시표를 생성해야 한다. 기본 자료형에 대해서는 해시 값을 생성하는 함수자가 이미 선언되어 있지만, 그 외의 자료형에 대해선 우리가 직접 만들어줘야 한다. std::unary_function을 이용해서 간단하게 인덱스의 두 값을 더하는 해시 함수자를 만든다.
namespace std
{
  template <>
  struct hash<array<int, 2> >: public unary_function<array<int, 2>, size_t>
  {
    size_t operator()(const array<int, 2>& idx) const
    {
      return accumulate(idx.begin(), idx.end(), 0);
    }
  };
}
이제 희소행렬 자체를 구현할 클래스를 선언한다. 행렬 연산을 구현하는 여러 함수도 구현해야겠지만, 여기선 행렬 원소의 읽기와 쓰기에 관련된 연산자 정도만 구현해보자.
template <typename T> class SparseMatrix
{
public:
  SparseMatrix(int i, int j): i_size(i), j_size(j) {}

  T& operator()(int i, int j)
  {
    std::array<int, 2> idx;
    idx[0] = i;
    idx[1] = j;
    return mat[idx];
  }

  T operator()(int i, int j) const
  {
    std::array<int, 2> idx;
    idx[0] = i;
    idx[1] = j;
    typename std::unordered_map<std::array<int, 2>, T>::const_iterator tmp;
    tmp = mat.find(idx);
   
    if (tmp == mat.end())
      return T();
    else
      return tmp->second;
  }
   
private:
  std::unordered_map<std::array<int, 2>, T> mat;
  std::size_t i_size, j_size;
};
눈여겨볼 점은 행렬의 원소를 설정할 때 사용할 operator()와 원소를 조회할 때 사용할 operator() const를 모두 선언해야 한다는 것이다. operator()만 선언한다면, 행렬의 원소를 조회할 때마다 조회하는 인덱스에 해당하는 항목을 map에 만들게 된다. 예를 들어,
SparseMatrix<double> sm(3, 3);
for (int i = 0; i < 3; ++i) {
    for (int j = 0; j < 3; ++j) {
      std::cout << sm(i, j) << " ";
    }
    std::cout << std::endl;
  }
처럼 모든 원소를 조회하기만 해도 내부 저장 장소인 mat에 모든 원소에 해당하는 항목이 만들어져 메모리를 낭비하게 된다.

실제로 원소를 설정하고 조회하는 방법은 아래와 같다.
using namespace std;

int main(int argc, char* argv[])
{
  SparseMatrix<double> sm(3, 3);
  sm(1, 2) = 1;
  sm(2, 0) = 2;

  const SparseMatrix<double>& sm_ref = sm;
  for (int i = 0; i < 3; ++i) {
    for (int j = 0; j < 3; ++j) {
      cout << sm_ref(i, j) << " ";
    }
    cout << endl;
  }

  return EXIT_SUCCESS;
}
operator() const를 호출하려면, 상수 레퍼런스나 상수 포인터를 이용해서 SpareMatrix에 접근해야 한다. 뭐, 대충 이렇다. 이 코드는 C++0x 표준인  std::arraystd::unordered_map을 사용하는데, 이 기능이 시험적으로 구현된 거라서 이용하면 컴파일 할 때 -std=c++0x 옵션을 줘야 한다.
$ g++ -Wall -g -pedantic -std=c++0x -o smat smat.cc
실행해보면, 잘 작동하는 것을 확인할 수 있다.
$ ./smat
0 0 0
0 0 1
2 0 0
$
mat의 크기를 검사해 보면, 단 두 개의 항목만 저장하고 있는 것을 확인할 수 있다. 간단한 희소행렬 완성!

2011년 8월 23일 화요일

C++의 반복자(iterator)를 이용하여 파이썬 생성자(generator) 만들기

최근에 GMES의 C++ 모듈 설계를 변경하면서, 내부 자료형으로 STL의 컨네이너를 사용하는 클래스가 필요하게 되었다. 이 글에서 설명하려는 내용과 관계 없는 부분을 제거하면, 대략적인 형태는 아래와 같다.
// itergen.hh
#include <vector>

class A
{
public:
  A(): a(3)
  {
    for (size_t i = 0; i < a.size(); i++)
      a[i] = i;
  }

  std::vector<int>::const_iterator
  begin() const
  {
    return a.begin();
  }

  std::vector<int>::const_iterator
  end() const
  {
    return a.end();
  }

private:
  std::vector<int> a;
};
// itergen.cc
#include "itergen.hh"
이 코드는 파이썬으로 래핑하여 사용하게 되는데, 아래와 같이 생성자로 이용하고자 한다. 어떻게 해야 할까?
>>> for i in A():
...    print i
1
2
3
파이썬 생성자에 관한 API를 살펴봤는데, 이거 잠깐 보고 구현할 난이도는 아닌 것 같다. 이왕 SWIG를 이용한다면, 뭔가 더 간단한 방법이 없을까? Reference Man의 블로그에 방법이 나와 있다. 이 블로그에 나와 있는 내용을 간단하게 정리해 보자.

일단, C++ 클래스에선 시작과 끝 부분의 반복자를 반환하는 begin, end 메서드만 제공하면 된다. 이는 일반적으로 STL에서 사용하는 방식이므로 C++ 클래스에서 제공하는 것이 자연스럽다. C++ 코드에서 할 일은 여기까지다. SWIG가 제공하는 %extend 키워드를 사용하면 C++ 코드를 더럽히지 않고도 파이썬 래핑에 필요한 코드를 추가할 수 있다. swig의 인터페이스 파일은 아래와 같다.
// itergen.i
%module itergen

%{
#include "itergen.hh"
%}

%extend A {
  std::vector<int>::const_iterator* _begin()
  {
    return new std::vector<int>::const_iterator($self->begin());
  }

  std::vector<int>::const_iterator* _end()
  {
    return new std::vector<int>::const_iterator($self->end());
  }

  int _deref(std::vector<int>::const_iterator* it)
  {
    return **it;
  }

  bool _compref(std::vector<int>::const_iterator* lhs,
  std::vector<int>::const_iterator* rhs)
  {
    return *lhs == *rhs;
  }

  void _incref(std::vector<int>::const_iterator* it)
  {
    ++(*it);
  }

  %pythoncode %{
    def __iter__(self):
      it = self._begin()
      while not self._compref(it, self._end()):
          yield self._deref(it)
          self._incref(it)
  %}
};

%include "itergen.hh"
파이썬은 STL의 반복자를 제대로 다루지 못하므로, 이를 처리해 주는 모든 메서드를 C++ 측에서 만들어줘야 한다. C++ 코드를 수정할 것 없이 인터페이스 파일에서 %extend 키워드를 이용해서 관련 메서드를 추가한다.

파이썬 클래스는 __iter__ 메서드를 선언해주면, 생성자로 이용할 수 있다. 이 메서드는 앞에서 만들어준 C++ 메서드를 이용하여 C++의 반복자를 처리한다. 원리는 간단하다. STL에서 흔히 사용하는, 반복자를 이용한 컨테이너 순회를 이용하는 데, 차이점이라면 값의 반환으로 return 대신에 yield를 사용한다는 점이다. yield를 사용하면, 함수의 값은 반환되는 데, 함수는 종료되지 않고 next 메서드가 호출될 때마다 yield를 호출한 다음 지점부터 연속해서 실행한다. __iter__ 메서드는 %pythoncode 키워드를 이용하여 파이썬 프록시 클래스에 추가한다.

컴파일 과정은 아래와 같다. 인터페이스 파일로부터 프록시 클래스와 파이썬 인터페이스를 생성하고, C++ 코드를 컴파일 한 후, 공유 라이브러리로 만들어 주면 된다.
$ swig -c++ -python itergen.i
$ g++ -Wall -g -I/usr/include/python2.6 -c itergen_wrap.cxx
$ g++ -shared itergen.o itergen_wrap.o -o _itergen.so
이제 파이썬 해석기에서 모듈을 올리고 생성자를 사용해보자.
$ python
Python 2.6.5 (r265:79063, Apr 16 2010, 13:09:56)
[GCC 4.4.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import itergen
>>> a=itergen.A()
>>> iter(a)
<generator object __iter__ at 0xb7720234>
>>> for i in a: print i
...
0
1
2
>>>
완료!

2011년 6월 27일 월요일

진화론에 관한 오해

진화론에 관해 친구와 대화한 내용을 정리해 보았습니다. 트위터로 나눈 대화라 기본적으로 질문과 답변이 좀 짧습니다. 옮기는 과정에서 수정과 추가 설명을 더했습니다.

  • 나: 진화론은 다른 과학 이론과는 다르게 믿는다는 표현을 쓰죠. 진화론, 쉬워 보이지만 주변에서 제대로 이해한 사람은 아직 한 사람도 만나보지 못했습니다. 다들 이해하려고 하지 않고 믿거나 믿지 않거나, 종교처럼 대합니다.
  • 친구: 저도 창조론자인데, 딴 건 모르겠고 다른 종이 생겨날 때가 진화론으론 이해하기 어려워서요. 창조론자들이 무조건 성경에다 끼워 맞추려고 해서 문제지 나름의 설득력이 있는 것도 많아요.
  • 나: 진화론은 종의 분화를 아주 잘 설명해줘요. 취약한 부분은 생명의 탄생 부분이죠. 리처드 도킨스의 책을 읽어보면 쉽게 알 수 있을 거예요.
  • 친구: 제가 보기엔 진화론 자체도 결함이 많아서요. 그게 창조론의 근거가 되죠. 창조론은 진화론이 틀렸으니까 창조론이 맞는다는 식으로 늘 주장해요.
  • 나: 아직, 결함이 없는 과학 이론은 없습니다. A 이론의 결함이 B 이론의 근거가 될 수는 없어요. 논리적 오류입니다.
  • 친구: 종이 갈라지는 시점이 언제냐가 젤 중요한 듯한데 조상과 생식을 할 수 없으면서 서로 생식 가능한 암수가 나와야 하는데 어떻게 가능한 건가요? 어째 한 쌍이 나왔다 치더라도 근친이라 번성하기 어려울 텐데요.
  • 나: 의 구분은 유전적으로 충분한 차이를 기반으로 하죠. 양자처럼 뚜렷이 구분되는 단위가 아니에요.
    침팬지와 인간도 생식할 수 없을 정도의 유전적 차이가 있지만, 그 사이를 이어주는 유전적 다리가 있었지요. 지금은 그 유전적 다리가 멸종해서 침팬지와 인간이 갑자기 튀어나온 것처럼 보이는 거죠.
    인간은 감각기관의 범위 안에서 사물을 판단하려는 습성이 있지요. 인간의 수명에 준하는 시간 단위론 종이 툭툭 튀어나오는 것 같지만, 지질학적 시간 단위로는 충분히 연속적인 유전적 변이가 일어납니다.
  • 친구: 이것도 창조론자들이 잘 걸고 넘는 것인데, 러시모아 산의 대통령 조각상을 많이 거론하죠. 오랜 시간이 지난다고 해서 스스로 저와 같은 작품이 만들어지는가? 마치 원시생물이 인간으로 진화한 거처럼요.
러시모어 산에 있는 대통령 조각상. (왼쪽부터) 조지 워싱턴, 토머스 제퍼슨, 시어도어 루스벨트, 에이브러햄 링컨.
  • 나: 진화는 누적된 변화입니다. 주사위를 6번 던져서 모두 6이 나올 확률은 6^-6이지만, 누적 시행으로는 1/6입니다. 조금 달라진 형질은 유전되고, 다음 세대에서 또 조금 달라지죠. 이게 바로 바위와는 다르게 생명체가 복잡한 형태로 진화한 이유입니다.
  • 친구: 미씽링크 개념인데 화석을 조사하다 보면 진화의 과정이 매우 불연속이란 걸 알 수 있어요. 침팬지와 사람으로 치면 십 퍼센트 사람 구십 퍼센트 침팬지 이십 퍼센트 사람 팔십 퍼센트 침팬지 이런 식으로 화석이 발견되어야 하는 게 맞지 싶은데요.
  • 나: 화석이 생성되는 조건은 매우 까다롭습니다. 이걸 우리가 발견하기도 어렵고요. 모든 종이 다 화석으로 남을 수는 없지요. 다행히 우리가 진화를 유추할 만큼의 화석은 발견되고 있습니다.
  • 친구: 진화론이 논쟁의 종지부를 찍으려면 새로운 종을 하나 만들어 보이면 될 텐데요.
  • 나: 새로운 종은 유전공학 실험실에서 매일 같이 만들어지고 있습니다. 하지만, 이게 진화론의 증거가 되지는 못합니다.
  • 친구: 종의 개념은 같은 종끼리 교배할 수 있어서 번성할 수 있느냐는 거죠. 무추는 씨앗이 없어서 스스로 번성할 수 없어서 종으로 볼 수 없고요. 제가 아는 한 종의 요건을 제대로 갖춘 새 생명체는 아직 없는 것으로 압니다.
  • 나: 유성생식을 하는 생물은 비용이 많이 들어서 쉽게 실험하지도 못합니다. 단세포 생물 등으로 주로 실험하겠죠. 이들이 생태계에 노출되면 생태계에 자리 잡을 수도 있을 겁니다. 다만, 이렇게 되면 생태계에 혼란을 가져올 수 있기에 철저히 격리합니다.
  • 친구: 제가 지적하는 건 진화론에서 아직 불충분하고 빠진 내용이 많다는 겁니다. 주요근거 들을 재현하기 매우 어렵기 때문에 논란은 계속 될겁니다.
  • 나: 진화론은 과학계에서 가장 완벽한 이론입니다. 진화론이 공격당하는 건 일부 종교의 신념과 맞지 않기 때문이지요. 진화론의 시간 단위가 인간의 시간 개념과 차이가 크다는 점과 어설픈 교육도 한몫하고 있습니다.
  • 친구: 진화론은 초중고 때부터 주구장창 했고 창조론은 한 학기 교양 수업이 다입니다. 교재는 시중에 나오는 얇은 단행본 두 권이고요. 창조론 반박 위주로 된 진화론 책은 없나요?
  • 나: 진화론은 생물학의 근간인데, 교육과정에선 이걸 따로 때어서 다윈과 적자생존만 언급하고 넘어가니 우리나라에 창조론이 넘쳐나는 데 지대한 공헌을 하고 있죠.
    진화론은 다윈이 1859년 출판한 책 《종의 기원》을 통해 널리 알려지게 되었다.
    책 목록은 리차드 도킨스 - Google 검색에 있습니다. 창조론에 쏟은 한 학기 정도의 관심만 진화론에 쏟으면 진화론이 불충분하고 빠진 내용이 많다는 오해는 해결할 수 있을 겁니다.

    2011년 6월 10일 금요일

    Windows 탐색기의 팝업 메뉴에 "IPython here" 추가하기

    윈도에서 파이썬을 사용하려면 EPD(Enthought Python Distribution)를 설치하는 게 가장 편하다. EPD는 파이썬 해석기뿐만 아니라, 여러 가지 파이썬 관련 라이브러리도 한 번에 설치할 수 있는 설치 파일을 제공한다. 교육용 버전과 체험판은 무료로 받을 수 있다.

    EPD를 설치한 후엔 PATH 환경변수를 설정해줘야 한다. 제어판->시스템 및 보안->시스템으로 들어간다. 왼편의 고급 시스템 설정을 누르면 시스템 속성 창이 뜬다. 고급 탭에서 환경 변수 단추를 클릭한다. 시스템 변수 중에서 Path를 선택하고 편집 단추를 클릭한다. 시스템 변수 편집 창이 뜨면 변수 값에 다음을 추가한다. 다음은 EPD를 C:\Program Files\Python26에 설치했을 경우를 가정한다.
    ;C:\Program Files\Python26;C:\Program Files\Python26\Scripts
    
    Path 환경변수에 파이썬 경로를 추가한다.

    이제 다음과 같은 ipython_here_shell_extension.reg 파일을 생성한다.
    Windows Registry Editor Version 5.00
    
    [HKEY_CLASSES_ROOT\Directory\shell\IPython here]
    
    [HKEY_CLASSES_ROOT\Directory\shell\IPython here\Command]
    @="cmd.exe /C ipython -p sh -i -c \"%cd %1\""
    
    이 파일을 더블 클릭하여 실행하면, 레지스트리가 수정된다. 이제 탐색기의 특정 폴더를 선택하고 우클릭을 하여 팝업메뉴를 띄우면 IPython here라는 메뉴를 볼 수 있다. 이 메뉴를 선택하면 해당 폴더에서 실행되는 IPython 셸이 뜬다.
    팝업 메뉴에 Ipython here가 나온다.
    경로명에 한글이 들어 있으면 작동하지 않는 문제가 있다.

    참고자료: http://lists.ipython.scipy.org/pipermail/ipython-user/2007-October/004746.html

    2011년 4월 1일 금요일

    미분방정식의 분석

    y'[t] == y[t] (4 - y[t])미분방정식을 분석해보자. 초기 조건을 y[0] == 0이나 y[0] == 4로 주면 y'[t]==0이니, t에 무관하게 각각 0과 4에 머무는 것을 예측할 수 있다. 더 구체적인 분석에 앞서, 백문이 불여일견이라 했으니 방향장(direction field)을 한 번 그려보자.
    StreamPlot[{1, y (4 - y)}, {t, 0, 5}, {y, -5, 5},
     FrameLabel -> {t, y}]
    
    y'=y(4-y)의 방향장 그래프
    y[0]을 초기 조건으로 잡는다면, y[0]이 0보다 크면, 4로 수렴하고 y[0]이 0보다 작으면 음의 무한대로 발산하는 것처럼 보인다. 과연 그럴까? 초기 조건을 y[0] == -2로 해서 미분방정식을 실제로 풀어보자.
    DSolve[{y'[t] == y[t] (4 - y[t]), y[0] == 4}, y[t], t]
    Equal @@ %[[1, 1]]
    Map[Limit[#, t -> Infinity] &, %]
    

    이게 어찌 된 일일까? 초기 조건을 0보다 작게 두었는데, 발산하지 않고 4로 수렴한다는 결과가 나왔다. 미분방정식의 해를 그래프로 그려보면 이유를 알 수 있다. 
    Plot[(4 Exp[4 t]))/(-3 + Exp[4 t]), {t, 0, 3}, PlotRange -> {-5, 9}, 
     AxesLabel -> {t, y}]
    
    y'=y(4-y)의 해를 그린 그래프

    y[t]는 -2에서 출발해서 Log[3]/4에서 음의 무한대로 발산한다. Log[3]/4의 오른쪽은 양의 무한대에서부터 시작해서 4로 수렴하게 된다. 즉 Log[3]/4라는 특이점(singularity)이 존재한다.

    2011년 2월 10일 목요일

    이맥스의 정규 표현식 치환 기능

    드디어 매스매티카 8을 쓸 수 있게 되었다. 이전 버전에서 작업하던 파일을 열어 실행하니 왠 걸. 떡하니 에러가 뜬다.

    매스매티카 8.0의 변경사항으로 인해 발생하는 오류


    난 매스매티카를 주로 이론적 분석, 즉 수식전개 등에 사용하는데, 종종 중간 결과를 수식번호 형식으로 저장해 놓는다. 해당 장(chapter)과 각 장에서의 수식번호를 나타내는 eq15\[Dash]8과 같은 형식의 변수명을 사용하는데, 여기서 문제가 발생했다. 이전 버전까지는 eq15-8을 단일 변수명으로 봤는데, 8.0에서는 이를 eq15, \[Dash], 8의 곱으로 인식한다.

    어차피 각 장은 별도의 파일에 저장하고 있으므로, 변수명에서 장 번호를 없애기로 했다. 그러면 변수명을 eq8과 같이 \[Dash] 없이 쓸 수 있으므로 문제를 해결할 수 있다. 파일 마다 많게는 100여개에 이르는 수식번호를 어떻게 일일이 다 바꿀까? 매스매티카에서 하기는 어려울 것 같다. 간단한 방법을 생각해보면, sed를 이용할 수도 있고 이맥스의 치환 기능을 이용할 수도 있겠다. 일단 변환할 문자열의 형태를 알아야 하니, 이 파일을 이맥스에서 열어보자.

    이 부분의 해석 방식이 바뀌었다.

    해당 부분의 입력이 위와 같이 저장된 것을 알 수 있다. 이제 이러한 패턴을 찾아서 바꾸면 된다. replace-regexp(C-M-%) 명령을 이용하면, 정규 표현식으로 위와 같은 패턴의 문자열을 모두 찾아서 바꿀 수 있다. 위 그림에서 강조한 부분을 나타내는 정규 표현식은 아래와 같다.
    RowBox\[{"eq15", "\\\[Dash\]", "[0-9][0-9]*"}\]
    
    하지만, 문제가 하나 있다. 패턴은 이게 맞는데 매번 치환마다 수식번호를 그것에 맞게 바꿔줘야 한다. 그렇지 않으면 모든 수식번호가 하나로 바뀌게 된다. 이건 패턴의 변수 기능을 이용하면 해결할 수 있다. 먼저, 수식번호에 해당하는 부분을 괄호로 둘러싼다. Query replace regexp:에 입력할 정규 표현식은 아래와 같다.
    RowBox\[{"eq15", "\\\[Dash\]", "\([0-9][0-9]*\)"}\]
    
    괄호로 둘러싼 부분은 치환된 문자열에서 \1로 불러올 수 있다. 괄호가 두 개 이상이면 \2, \3 등으로 불러올 수 있다. 즉, with:에 입력할 패턴은 아래와 같다.
    "eq\1"
    
    일치하는 문자열을 한번에 모두 치환하려면 Query replacing이 진행 중일 때, !를 입력하면 된다.

    replace-regexp를이용하여 치환하는 모습

    문자열 치환을 마치고, 파일을 저장한다. 이제 수정한 파일을 매스매티카에서 열어보자.

    노트북 파일을 매스매티카 외부에서 수정하면 저장된 임시 계산 결과를 이용할 수 없다.

    경고창이 하나 뜨는며 살짝 겁을 준다. 잘 읽어보면, 수정된 파일이므로 임시 저장 결과를 이용할 수 없다는 안내이다. 친절하게도 내용에는 문제가 없을 거라며 안심 시켜준다. OK를 클릭해서 넘어간다. 수식을 다시 실행해보면, 문제가 해결된 것을 볼 수 있다.

    이맥스와 정규 표현식을 이용하면 문제가 되는 부분을 한 번에 모두 치환할 수 있다.